Implementar sistema completo de trazabilidad de lotes
Some checks failed
build-and-deploy / build-and-deploy (push) Failing after 1m47s
Some checks failed
build-and-deploy / build-and-deploy (push) Failing after 1m47s
- Agregar PostgreSQL 16 con esquema completo - Crear API endpoints para lotes y operaciones - Implementar UI con Nuxt UI (tablas, formularios, trazabilidad) - Agregar datos de ejemplo del flujo completo - Documentar sistema en PLAN_TRAZABILIDAD.md
This commit is contained in:
225
nuxt4/server/database/01_schema.sql
Normal file
225
nuxt4/server/database/01_schema.sql
Normal file
@@ -0,0 +1,225 @@
|
||||
-- =====================================================
|
||||
-- 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 $$;
|
||||
385
nuxt4/server/database/02_seed.sql
Normal file
385
nuxt4/server/database/02_seed.sql
Normal file
@@ -0,0 +1,385 @@
|
||||
-- =====================================================
|
||||
-- DATOS DE EJEMPLO - FLUJO COMPLETO DE TRAZABILIDAD
|
||||
-- =====================================================
|
||||
-- Este script crea un ejemplo completo del flujo de café desde
|
||||
-- ingreso de uva hasta secado final, incluyendo ajustes y correcciones.
|
||||
--
|
||||
-- Flujo principal:
|
||||
-- Ingreso uva → Despulpado → Oreado → Ajuste merma → Ajuste tipo →
|
||||
-- Presecado → Reposo → Secado (mezcla con otro reposo)
|
||||
|
||||
-- Limpiar datos existentes (solo para demo/desarrollo)
|
||||
DO $$
|
||||
BEGIN
|
||||
RAISE NOTICE 'Limpiando datos de ejemplo previos...';
|
||||
END $$;
|
||||
|
||||
TRUNCATE TABLE operacion_lotes, operaciones, lotes CASCADE;
|
||||
|
||||
-- =====================================================
|
||||
-- PASO 1: INGRESO DE UVA
|
||||
-- =====================================================
|
||||
-- Llega café uva de un productor
|
||||
|
||||
DO $$
|
||||
DECLARE
|
||||
op_ingreso_id UUID;
|
||||
lote_uva_id UUID;
|
||||
BEGIN
|
||||
RAISE NOTICE 'Creando ingreso de uva...';
|
||||
|
||||
-- Crear operación de ingreso
|
||||
INSERT INTO operaciones (tipo, fecha, meta)
|
||||
VALUES (
|
||||
'ingreso',
|
||||
NOW() - INTERVAL '10 days',
|
||||
'{"productor": "Finca El Roble", "lote_productor": "2024-11-A"}'::jsonb
|
||||
)
|
||||
RETURNING id INTO op_ingreso_id;
|
||||
|
||||
-- Crear lote de uva
|
||||
INSERT INTO lotes (codigo, tipo, fecha_creado, cantidad_kg, meta)
|
||||
VALUES (
|
||||
'UVA-001',
|
||||
'uva',
|
||||
NOW() - INTERVAL '10 days',
|
||||
2086,
|
||||
'{"variedad": "Caturra", "procedencia": "Finca El Roble"}'::jsonb
|
||||
)
|
||||
RETURNING id INTO lote_uva_id;
|
||||
|
||||
-- Relacionar: operación de ingreso → lote de uva (output)
|
||||
INSERT INTO operacion_lotes (operacion_id, lote_id, rol, cantidad_kg)
|
||||
VALUES (op_ingreso_id, lote_uva_id, 'output', 2086);
|
||||
|
||||
END $$;
|
||||
|
||||
|
||||
-- =====================================================
|
||||
-- PASO 2: DESPULPADO
|
||||
-- =====================================================
|
||||
-- Se despulpa la uva y se obtienen tres lotes: primera, segunda y rechazos
|
||||
|
||||
DO $$
|
||||
DECLARE
|
||||
op_despulpado_id UUID;
|
||||
lote_uva_id UUID;
|
||||
lote_primera_id UUID;
|
||||
lote_segunda_id UUID;
|
||||
lote_rechazos_id UUID;
|
||||
BEGIN
|
||||
RAISE NOTICE 'Creando despulpado...';
|
||||
|
||||
-- Obtener ID del lote de uva
|
||||
SELECT id INTO lote_uva_id FROM lotes WHERE codigo = 'UVA-001';
|
||||
|
||||
-- Crear operación de despulpado
|
||||
INSERT INTO operaciones (tipo, fecha, meta)
|
||||
VALUES (
|
||||
'despulpado',
|
||||
NOW() - INTERVAL '9 days',
|
||||
'{"pila": 2, "operador": "Juan Pérez"}'::jsonb
|
||||
)
|
||||
RETURNING id INTO op_despulpado_id;
|
||||
|
||||
-- Crear lotes de salida
|
||||
INSERT INTO lotes (codigo, tipo, fecha_creado, cantidad_kg, meta)
|
||||
VALUES
|
||||
('PRIM-001', 'despulpado_primera', NOW() - INTERVAL '9 days', 1500, '{"calidad": "A"}'::jsonb),
|
||||
('SEG-001', 'despulpado_segunda', NOW() - INTERVAL '9 days', 400, '{"calidad": "B"}'::jsonb),
|
||||
('RECH-001', 'despulpado_rechazos', NOW() - INTERVAL '9 days', 150, '{"destino": "compost"}'::jsonb)
|
||||
RETURNING id INTO lote_primera_id, lote_segunda_id, lote_rechazos_id;
|
||||
|
||||
-- Relacionar: uva → despulpado (input)
|
||||
INSERT INTO operacion_lotes (operacion_id, lote_id, rol, cantidad_kg)
|
||||
VALUES (op_despulpado_id, lote_uva_id, 'input', 2086);
|
||||
|
||||
-- Relacionar: despulpado → primera, segunda, rechazos (outputs)
|
||||
INSERT INTO operacion_lotes (operacion_id, lote_id, rol, cantidad_kg)
|
||||
VALUES
|
||||
(op_despulpado_id, lote_primera_id, 'output', 1500),
|
||||
(op_despulpado_id, lote_segunda_id, 'output', 400),
|
||||
(op_despulpado_id, lote_rechazos_id, 'output', 150);
|
||||
|
||||
END $$;
|
||||
|
||||
|
||||
-- =====================================================
|
||||
-- PASO 3: OREADO (con error en registro)
|
||||
-- =====================================================
|
||||
-- Se orea el lote de primera calidad, pero se registra mal la cantidad
|
||||
|
||||
DO $$
|
||||
DECLARE
|
||||
op_oreado_id UUID;
|
||||
lote_primera_id UUID;
|
||||
lote_oreado_id UUID;
|
||||
BEGIN
|
||||
RAISE NOTICE 'Creando oreado (con error de registro)...';
|
||||
|
||||
-- Obtener ID del lote de primera
|
||||
SELECT id INTO lote_primera_id FROM lotes WHERE codigo = 'PRIM-001';
|
||||
|
||||
-- Crear operación de oreado
|
||||
INSERT INTO operaciones (tipo, fecha, meta)
|
||||
VALUES (
|
||||
'oreado',
|
||||
NOW() - INTERVAL '8 days',
|
||||
'{"patio": 1, "inicio": "06:00", "fin": "18:00"}'::jsonb
|
||||
)
|
||||
RETURNING id INTO op_oreado_id;
|
||||
|
||||
-- Crear lote oreado (cantidad mal registrada: debería ser menos por merma)
|
||||
INSERT INTO lotes (codigo, tipo, fecha_creado, cantidad_kg, meta)
|
||||
VALUES (
|
||||
'ORE-001',
|
||||
'oreado',
|
||||
NOW() - INTERVAL '8 days',
|
||||
1500, -- Error: debería ser 1480 kg
|
||||
'{"humedad_inicial": 55, "humedad_final": 45}'::jsonb
|
||||
)
|
||||
RETURNING id INTO lote_oreado_id;
|
||||
|
||||
-- Relacionar
|
||||
INSERT INTO operacion_lotes (operacion_id, lote_id, rol, cantidad_kg)
|
||||
VALUES
|
||||
(op_oreado_id, lote_primera_id, 'input', 1500),
|
||||
(op_oreado_id, lote_oreado_id, 'output', 1500);
|
||||
|
||||
END $$;
|
||||
|
||||
|
||||
-- =====================================================
|
||||
-- PASO 4: AJUSTE DE MERMA
|
||||
-- =====================================================
|
||||
-- Se corrige la cantidad: realmente hubo merma de 20 kg
|
||||
|
||||
DO $$
|
||||
DECLARE
|
||||
op_ajuste_id UUID;
|
||||
lote_oreado_id UUID;
|
||||
lote_oreado_corr_id UUID;
|
||||
BEGIN
|
||||
RAISE NOTICE 'Aplicando ajuste de merma...';
|
||||
|
||||
-- Obtener ID del lote oreado
|
||||
SELECT id INTO lote_oreado_id FROM lotes WHERE codigo = 'ORE-001';
|
||||
|
||||
-- Crear operación de ajuste
|
||||
INSERT INTO operaciones (tipo, fecha, meta)
|
||||
VALUES (
|
||||
'ajuste_merma',
|
||||
NOW() - INTERVAL '7 days',
|
||||
'{"motivo": "Corrección de pesaje", "merma_kg": 20}'::jsonb
|
||||
)
|
||||
RETURNING id INTO op_ajuste_id;
|
||||
|
||||
-- Crear lote corregido
|
||||
INSERT INTO lotes (codigo, tipo, fecha_creado, cantidad_kg, meta)
|
||||
VALUES (
|
||||
'ORE-001A',
|
||||
'oreado',
|
||||
NOW() - INTERVAL '7 days',
|
||||
1480,
|
||||
'{"humedad": 45, "corregido": true}'::jsonb
|
||||
)
|
||||
RETURNING id INTO lote_oreado_corr_id;
|
||||
|
||||
-- Relacionar
|
||||
INSERT INTO operacion_lotes (operacion_id, lote_id, rol, cantidad_kg)
|
||||
VALUES
|
||||
(op_ajuste_id, lote_oreado_id, 'input', 1500),
|
||||
(op_ajuste_id, lote_oreado_corr_id, 'output', 1480);
|
||||
|
||||
END $$;
|
||||
|
||||
|
||||
-- =====================================================
|
||||
-- PASO 5: AJUSTE DE TIPO
|
||||
-- =====================================================
|
||||
-- Se descubre que en realidad era presecado, no oreado
|
||||
|
||||
DO $$
|
||||
DECLARE
|
||||
op_ajuste_tipo_id UUID;
|
||||
lote_oreado_corr_id UUID;
|
||||
lote_presecado_id UUID;
|
||||
BEGIN
|
||||
RAISE NOTICE 'Aplicando ajuste de tipo...';
|
||||
|
||||
-- Obtener ID del lote oreado corregido
|
||||
SELECT id INTO lote_oreado_corr_id FROM lotes WHERE codigo = 'ORE-001A';
|
||||
|
||||
-- Crear operación de ajuste de tipo
|
||||
INSERT INTO operaciones (tipo, fecha, meta)
|
||||
VALUES (
|
||||
'ajuste_tipo',
|
||||
NOW() - INTERVAL '6 days',
|
||||
'{"motivo": "Revisión de proceso", "tipo_anterior": "oreado", "tipo_nuevo": "presecado"}'::jsonb
|
||||
)
|
||||
RETURNING id INTO op_ajuste_tipo_id;
|
||||
|
||||
-- Crear lote con tipo correcto
|
||||
INSERT INTO lotes (codigo, tipo, fecha_creado, cantidad_kg, meta)
|
||||
VALUES (
|
||||
'PRE-001',
|
||||
'presecado',
|
||||
NOW() - INTERVAL '6 days',
|
||||
1480,
|
||||
'{"humedad": 45, "tipo_corregido": true}'::jsonb
|
||||
)
|
||||
RETURNING id INTO lote_presecado_id;
|
||||
|
||||
-- Relacionar
|
||||
INSERT INTO operacion_lotes (operacion_id, lote_id, rol, cantidad_kg)
|
||||
VALUES
|
||||
(op_ajuste_tipo_id, lote_oreado_corr_id, 'input', 1480),
|
||||
(op_ajuste_tipo_id, lote_presecado_id, 'output', 1480);
|
||||
|
||||
END $$;
|
||||
|
||||
|
||||
-- =====================================================
|
||||
-- PASO 6: REPOSO
|
||||
-- =====================================================
|
||||
-- El presecado pasa a reposo
|
||||
|
||||
DO $$
|
||||
DECLARE
|
||||
op_reposo_id UUID;
|
||||
lote_presecado_id UUID;
|
||||
lote_reposo_id UUID;
|
||||
BEGIN
|
||||
RAISE NOTICE 'Creando reposo...';
|
||||
|
||||
-- Obtener ID del lote presecado
|
||||
SELECT id INTO lote_presecado_id FROM lotes WHERE codigo = 'PRE-001';
|
||||
|
||||
-- Crear operación de reposo
|
||||
INSERT INTO operaciones (tipo, fecha, meta)
|
||||
VALUES (
|
||||
'reposo',
|
||||
NOW() - INTERVAL '5 days',
|
||||
'{"area": "Bodega A", "dias_reposo": 3}'::jsonb
|
||||
)
|
||||
RETURNING id INTO op_reposo_id;
|
||||
|
||||
-- Crear lote en reposo
|
||||
INSERT INTO lotes (codigo, tipo, fecha_creado, cantidad_kg, meta)
|
||||
VALUES (
|
||||
'REP-001',
|
||||
'reposo',
|
||||
NOW() - INTERVAL '5 days',
|
||||
1480,
|
||||
'{"humedad": 43}'::jsonb
|
||||
)
|
||||
RETURNING id INTO lote_reposo_id;
|
||||
|
||||
-- Relacionar
|
||||
INSERT INTO operacion_lotes (operacion_id, lote_id, rol, cantidad_kg)
|
||||
VALUES
|
||||
(op_reposo_id, lote_presecado_id, 'input', 1480),
|
||||
(op_reposo_id, lote_reposo_id, 'output', 1480);
|
||||
|
||||
END $$;
|
||||
|
||||
|
||||
-- =====================================================
|
||||
-- PASO 7: SEGUNDO FLUJO (para mezclar en secado)
|
||||
-- =====================================================
|
||||
-- Crear otro lote de reposo de un proceso paralelo
|
||||
|
||||
DO $$
|
||||
DECLARE
|
||||
lote_reposo2_id UUID;
|
||||
BEGIN
|
||||
RAISE NOTICE 'Creando segundo lote de reposo (proceso paralelo)...';
|
||||
|
||||
-- Crear lote de reposo directamente (proceso simplificado)
|
||||
INSERT INTO lotes (codigo, tipo, fecha_creado, cantidad_kg, meta)
|
||||
VALUES (
|
||||
'REP-002',
|
||||
'reposo',
|
||||
NOW() - INTERVAL '4 days',
|
||||
520,
|
||||
'{"humedad": 42, "origen": "Proceso B"}'::jsonb
|
||||
)
|
||||
RETURNING id INTO lote_reposo2_id;
|
||||
|
||||
END $$;
|
||||
|
||||
|
||||
-- =====================================================
|
||||
-- PASO 8: SECADO (MEZCLA DE DOS REPOSOS)
|
||||
-- =====================================================
|
||||
-- Se mezclan REP-001 y REP-002 para el secado final
|
||||
|
||||
DO $$
|
||||
DECLARE
|
||||
op_secado_id UUID;
|
||||
lote_reposo1_id UUID;
|
||||
lote_reposo2_id UUID;
|
||||
lote_secado_id UUID;
|
||||
BEGIN
|
||||
RAISE NOTICE 'Creando secado (mezcla de reposos)...';
|
||||
|
||||
-- Obtener IDs de los lotes de reposo
|
||||
SELECT id INTO lote_reposo1_id FROM lotes WHERE codigo = 'REP-001';
|
||||
SELECT id INTO lote_reposo2_id FROM lotes WHERE codigo = 'REP-002';
|
||||
|
||||
-- Crear operación de secado
|
||||
INSERT INTO operaciones (tipo, fecha, meta)
|
||||
VALUES (
|
||||
'secado',
|
||||
NOW() - INTERVAL '2 days',
|
||||
'{"secadora": "Solar 1", "temperatura_max": 45, "dias": 7}'::jsonb
|
||||
)
|
||||
RETURNING id INTO op_secado_id;
|
||||
|
||||
-- Crear lote secado final
|
||||
INSERT INTO lotes (codigo, tipo, fecha_creado, cantidad_kg, meta)
|
||||
VALUES (
|
||||
'SEC-001',
|
||||
'secado',
|
||||
NOW() - INTERVAL '2 days',
|
||||
2000,
|
||||
'{"humedad_final": 11.5, "calidad": "Pergamino seco"}'::jsonb
|
||||
)
|
||||
RETURNING id INTO lote_secado_id;
|
||||
|
||||
-- Relacionar: dos reposos como input
|
||||
INSERT INTO operacion_lotes (operacion_id, lote_id, rol, cantidad_kg)
|
||||
VALUES
|
||||
(op_secado_id, lote_reposo1_id, 'input', 1480),
|
||||
(op_secado_id, lote_reposo2_id, 'input', 520);
|
||||
|
||||
-- Relacionar: secado como output
|
||||
INSERT INTO operacion_lotes (operacion_id, lote_id, rol, cantidad_kg)
|
||||
VALUES (op_secado_id, lote_secado_id, 'output', 2000);
|
||||
|
||||
END $$;
|
||||
|
||||
|
||||
-- =====================================================
|
||||
-- RESUMEN DE DATOS CREADOS
|
||||
-- =====================================================
|
||||
DO $$
|
||||
DECLARE
|
||||
total_lotes INTEGER;
|
||||
total_operaciones INTEGER;
|
||||
total_relaciones INTEGER;
|
||||
BEGIN
|
||||
SELECT COUNT(*) INTO total_lotes FROM lotes;
|
||||
SELECT COUNT(*) INTO total_operaciones FROM operaciones;
|
||||
SELECT COUNT(*) INTO total_relaciones FROM operacion_lotes;
|
||||
|
||||
RAISE NOTICE '';
|
||||
RAISE NOTICE '✓ Datos de ejemplo creados exitosamente';
|
||||
RAISE NOTICE ' - % lotes creados', total_lotes;
|
||||
RAISE NOTICE ' - % operaciones creadas', total_operaciones;
|
||||
RAISE NOTICE ' - % relaciones lote-operación creadas', total_relaciones;
|
||||
RAISE NOTICE '';
|
||||
RAISE NOTICE 'Lote final: SEC-001 (Secado)';
|
||||
RAISE NOTICE 'Puedes consultar su trazabilidad completa con:';
|
||||
RAISE NOTICE ' SELECT * FROM get_trazabilidad((SELECT id FROM lotes WHERE codigo = ''SEC-001''));';
|
||||
END $$;
|
||||
338
nuxt4/server/database/README.md
Normal file
338
nuxt4/server/database/README.md
Normal file
@@ -0,0 +1,338 @@
|
||||
# Database - Scripts SQL
|
||||
|
||||
Este directorio contiene los scripts SQL para inicializar y gestionar la base de datos PostgreSQL del sistema de trazabilidad.
|
||||
|
||||
---
|
||||
|
||||
## Archivos
|
||||
|
||||
### `01_schema.sql`
|
||||
Crea el esquema completo de la base de datos:
|
||||
- Tablas: `lotes`, `operaciones`, `operacion_lotes`
|
||||
- Índices para optimización
|
||||
- Función `get_trazabilidad()` para queries recursivas
|
||||
- Vista `vista_lotes_con_origen`
|
||||
- Constraints y validaciones
|
||||
|
||||
### `02_seed.sql`
|
||||
Datos de ejemplo que representan un flujo completo:
|
||||
- Ingreso de uva (2086 kg)
|
||||
- Despulpado → primera, segunda, rechazos
|
||||
- Oreado (con error de registro)
|
||||
- Ajuste de merma (1500 → 1480 kg)
|
||||
- Ajuste de tipo (oreado → presecado)
|
||||
- Reposo
|
||||
- Secado final (mezcla de 2 lotes = 2000 kg)
|
||||
|
||||
---
|
||||
|
||||
## Ejecución Automática
|
||||
|
||||
Cuando usas **Docker Compose**, estos scripts se ejecutan automáticamente al iniciar PostgreSQL por primera vez gracias al montaje:
|
||||
|
||||
```yaml
|
||||
volumes:
|
||||
- ./nuxt4/server/database:/docker-entrypoint-initdb.d:ro
|
||||
```
|
||||
|
||||
PostgreSQL ejecuta todos los archivos `.sql` en orden alfabético dentro de `/docker-entrypoint-initdb.d/`.
|
||||
|
||||
**Orden de ejecución:**
|
||||
1. `01_schema.sql` - Crea estructura
|
||||
2. `02_seed.sql` - Inserta datos de ejemplo
|
||||
|
||||
---
|
||||
|
||||
## Ejecución Manual
|
||||
|
||||
### Opción 1: Desde el contenedor Docker
|
||||
|
||||
```bash
|
||||
# Conectarse al contenedor
|
||||
docker exec -it seguidorDeLotes-postgres psql -U seguidor -d seguidor_lotes
|
||||
|
||||
# Dentro de psql, ejecutar:
|
||||
\i /docker-entrypoint-initdb.d/01_schema.sql
|
||||
\i /docker-entrypoint-initdb.d/02_seed.sql
|
||||
```
|
||||
|
||||
### Opción 2: Desde tu máquina local
|
||||
|
||||
```bash
|
||||
# Asegúrate de tener psql instalado
|
||||
psql -h localhost -U seguidor -d seguidor_lotes -f 01_schema.sql
|
||||
psql -h localhost -U seguidor -d seguidor_lotes -f 02_seed.sql
|
||||
```
|
||||
|
||||
### Opción 3: Copiar y pegar en pgAdmin o DBeaver
|
||||
|
||||
Abre los archivos `.sql` en tu cliente SQL favorito y ejecútalos directamente.
|
||||
|
||||
---
|
||||
|
||||
## Reiniciar la Base de Datos
|
||||
|
||||
Si necesitas empezar desde cero:
|
||||
|
||||
```bash
|
||||
# Detener contenedores y eliminar volúmenes
|
||||
docker-compose down -v
|
||||
|
||||
# Volver a iniciar (ejecutará scripts automáticamente)
|
||||
docker-compose up -d
|
||||
|
||||
# Ver logs para confirmar
|
||||
docker logs -f seguidorDeLotes-postgres
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Verificar que todo está correcto
|
||||
|
||||
### 1. Conectarse a la base de datos
|
||||
|
||||
```bash
|
||||
docker exec -it seguidorDeLotes-postgres psql -U seguidor -d seguidor_lotes
|
||||
```
|
||||
|
||||
### 2. Listar tablas
|
||||
|
||||
```sql
|
||||
\dt
|
||||
```
|
||||
|
||||
**Deberías ver:**
|
||||
```
|
||||
List of relations
|
||||
Schema | Name | Type | Owner
|
||||
--------+------------------+-------+----------
|
||||
public | lotes | table | seguidor
|
||||
public | operacion_lotes | table | seguidor
|
||||
public | operaciones | table | seguidor
|
||||
```
|
||||
|
||||
### 3. Contar registros
|
||||
|
||||
```sql
|
||||
SELECT
|
||||
(SELECT COUNT(*) FROM lotes) as total_lotes,
|
||||
(SELECT COUNT(*) FROM operaciones) as total_operaciones,
|
||||
(SELECT COUNT(*) FROM operacion_lotes) as total_relaciones;
|
||||
```
|
||||
|
||||
**Deberías ver algo como:**
|
||||
```
|
||||
total_lotes | total_operaciones | total_relaciones
|
||||
-------------+-------------------+------------------
|
||||
9 | 8 | 16
|
||||
```
|
||||
|
||||
### 4. Ver lotes creados
|
||||
|
||||
```sql
|
||||
SELECT codigo, tipo, cantidad_kg FROM lotes ORDER BY fecha_creado;
|
||||
```
|
||||
|
||||
**Deberías ver:**
|
||||
```
|
||||
codigo | tipo | cantidad_kg
|
||||
-----------+----------------------+-------------
|
||||
UVA-001 | uva | 2086.00
|
||||
PRIM-001 | despulpado_primera | 1500.00
|
||||
SEG-001 | despulpado_segunda | 400.00
|
||||
RECH-001 | despulpado_rechazos | 150.00
|
||||
ORE-001 | oreado | 1500.00
|
||||
ORE-001A | oreado | 1480.00
|
||||
PRE-001 | presecado | 1480.00
|
||||
REP-001 | reposo | 1480.00
|
||||
REP-002 | reposo | 520.00
|
||||
SEC-001 | secado | 2000.00
|
||||
```
|
||||
|
||||
### 5. Probar la función de trazabilidad
|
||||
|
||||
```sql
|
||||
SELECT * FROM get_trazabilidad(
|
||||
(SELECT id FROM lotes WHERE codigo = 'SEC-001')
|
||||
);
|
||||
```
|
||||
|
||||
**Deberías ver:** Todo el historial del lote `SEC-001` desde el ingreso de uva.
|
||||
|
||||
---
|
||||
|
||||
## Estructura de las Tablas
|
||||
|
||||
### Tabla `lotes`
|
||||
|
||||
```sql
|
||||
CREATE TABLE lotes (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
codigo TEXT UNIQUE,
|
||||
tipo TEXT NOT NULL,
|
||||
fecha_creado TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
lugar_id INTEGER,
|
||||
cantidad_kg NUMERIC(10,2),
|
||||
meta JSONB
|
||||
);
|
||||
```
|
||||
|
||||
### Tabla `operaciones`
|
||||
|
||||
```sql
|
||||
CREATE TABLE operaciones (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
tipo TEXT NOT NULL,
|
||||
fecha TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
lugar_id INTEGER,
|
||||
meta JSONB
|
||||
);
|
||||
```
|
||||
|
||||
### Tabla `operacion_lotes`
|
||||
|
||||
```sql
|
||||
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 CHECK (rol IN ('input', 'output')),
|
||||
cantidad_kg NUMERIC(10,2),
|
||||
PRIMARY KEY (operacion_id, lote_id, rol)
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Queries Útiles
|
||||
|
||||
### Ver todas las operaciones de un lote
|
||||
|
||||
```sql
|
||||
SELECT
|
||||
o.tipo AS operacion,
|
||||
o.fecha,
|
||||
ol.rol,
|
||||
ol.cantidad_kg
|
||||
FROM operacion_lotes ol
|
||||
JOIN operaciones o ON o.id = ol.operacion_id
|
||||
WHERE ol.lote_id = (SELECT id FROM lotes WHERE codigo = 'SEC-001')
|
||||
ORDER BY o.fecha;
|
||||
```
|
||||
|
||||
### Ver lotes que se usaron para crear un lote específico (inputs directos)
|
||||
|
||||
```sql
|
||||
-- Inputs directos del lote SEC-001
|
||||
SELECT
|
||||
l.codigo,
|
||||
l.tipo,
|
||||
l.cantidad_kg,
|
||||
o.tipo AS operacion_tipo
|
||||
FROM lotes l
|
||||
JOIN operacion_lotes ol_in ON ol_in.lote_id = l.id
|
||||
JOIN operacion_lotes ol_out ON ol_out.operacion_id = ol_in.operacion_id
|
||||
JOIN operaciones o ON o.id = ol_out.operacion_id
|
||||
WHERE ol_out.lote_id = (SELECT id FROM lotes WHERE codigo = 'SEC-001')
|
||||
AND ol_out.rol = 'output'
|
||||
AND ol_in.rol = 'input';
|
||||
```
|
||||
|
||||
### Ver estadísticas de un período
|
||||
|
||||
```sql
|
||||
SELECT
|
||||
tipo,
|
||||
COUNT(*) as total,
|
||||
SUM(cantidad_kg) as kg_totales
|
||||
FROM operaciones
|
||||
WHERE fecha >= NOW() - INTERVAL '30 days'
|
||||
GROUP BY tipo
|
||||
ORDER BY total DESC;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Migraciones Futuras
|
||||
|
||||
Cuando necesites hacer cambios al esquema en producción:
|
||||
|
||||
1. **Crear archivo de migración** (ej: `03_add_lugares_table.sql`)
|
||||
2. **NO modificar** `01_schema.sql` ni `02_seed.sql` directamente
|
||||
3. **Aplicar migración manualmente** en producción
|
||||
|
||||
Ejemplo de migración:
|
||||
|
||||
```sql
|
||||
-- 03_add_lugares_table.sql
|
||||
CREATE TABLE IF NOT EXISTS lugares (
|
||||
id SERIAL PRIMARY KEY,
|
||||
nombre TEXT NOT NULL,
|
||||
tipo TEXT, -- patio, pila, bodega, etc.
|
||||
capacidad_kg NUMERIC
|
||||
);
|
||||
|
||||
-- Agregar foreign key a lotes
|
||||
ALTER TABLE lotes
|
||||
ADD CONSTRAINT fk_lotes_lugar
|
||||
FOREIGN KEY (lugar_id) REFERENCES lugares(id);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Backup y Restore
|
||||
|
||||
### Hacer backup
|
||||
|
||||
```bash
|
||||
docker exec seguidorDeLotes-postgres pg_dump -U seguidor seguidor_lotes > backup_$(date +%Y%m%d).sql
|
||||
```
|
||||
|
||||
### Restaurar backup
|
||||
|
||||
```bash
|
||||
cat backup_20251121.sql | docker exec -i seguidorDeLotes-postgres psql -U seguidor -d seguidor_lotes
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "relation lotes does not exist"
|
||||
|
||||
Los scripts no se ejecutaron. Verificar:
|
||||
```bash
|
||||
docker logs seguidorDeLotes-postgres
|
||||
```
|
||||
|
||||
Si ves errores, eliminar volumen y reiniciar:
|
||||
```bash
|
||||
docker-compose down -v
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
### "permission denied for schema public"
|
||||
|
||||
Problema de permisos. Conectarse como superuser:
|
||||
```bash
|
||||
docker exec -it seguidorDeLotes-postgres psql -U postgres -d seguidor_lotes
|
||||
|
||||
-- Dar permisos
|
||||
GRANT ALL ON SCHEMA public TO seguidor;
|
||||
GRANT ALL ON ALL TABLES IN SCHEMA public TO seguidor;
|
||||
```
|
||||
|
||||
### Los datos de ejemplo se duplican
|
||||
|
||||
`02_seed.sql` hace `TRUNCATE` al inicio. Si no quieres perder datos, comenta esa línea.
|
||||
|
||||
---
|
||||
|
||||
## Referencias
|
||||
|
||||
- [PostgreSQL JSON Functions](https://www.postgresql.org/docs/current/functions-json.html)
|
||||
- [Recursive Queries (CTE)](https://www.postgresql.org/docs/current/queries-with.html)
|
||||
- [Docker Init Scripts](https://hub.docker.com/_/postgres)
|
||||
|
||||
---
|
||||
|
||||
**Última actualización**: 2025-11-21
|
||||
Reference in New Issue
Block a user