T2.1: Renamed warehouse_type -> construction_warehouse_type
in almacenes_proyecto to avoid naming conflict with ERP-Core
T2.3: Added user_id column to hr.employees
- Optional FK to auth.users (employee may not have system access)
- Unique constraint for 1:1 mapping
- Conditional FK (only created if auth.users exists)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
339 lines
16 KiB
SQL
339 lines
16 KiB
SQL
-- ============================================================================
|
|
-- INVENTORY EXTENSION Schema DDL - Extensiones de Inventario para Construcción
|
|
-- Modulos: MAI-004 (Compras e Inventarios)
|
|
-- Version: 1.0.0
|
|
-- Fecha: 2025-12-08
|
|
-- ============================================================================
|
|
-- TIPO: Extensión del ERP Core (MGN-005 Inventory)
|
|
-- NOTA: Contiene SOLO extensiones específicas de construcción.
|
|
-- Las tablas base están en el ERP Core.
|
|
-- ============================================================================
|
|
-- PREREQUISITOS:
|
|
-- 1. ERP-Core instalado (auth.tenants, auth.users)
|
|
-- 2. Schema construction instalado (fraccionamientos, conceptos, lotes, departamentos)
|
|
-- 3. Schema inventory de ERP-Core instalado (opcional, para FKs)
|
|
-- ============================================================================
|
|
|
|
-- Verificar prerequisitos
|
|
DO $$
|
|
BEGIN
|
|
IF NOT EXISTS (SELECT 1 FROM pg_namespace WHERE nspname = 'auth') THEN
|
|
RAISE EXCEPTION 'Schema auth no existe. Ejecutar primero ERP-Core DDL';
|
|
END IF;
|
|
IF NOT EXISTS (SELECT 1 FROM pg_namespace WHERE nspname = 'construction') THEN
|
|
RAISE EXCEPTION 'Schema construction no existe. Ejecutar primero construction DDL';
|
|
END IF;
|
|
END $$;
|
|
|
|
-- Crear schema si no existe (puede ya existir desde ERP-Core)
|
|
CREATE SCHEMA IF NOT EXISTS inventory;
|
|
|
|
-- ============================================================================
|
|
-- TYPES (ENUMs) ADICIONALES
|
|
-- ============================================================================
|
|
|
|
DO $$ BEGIN
|
|
CREATE TYPE inventory.warehouse_type_construction AS ENUM (
|
|
'central', 'obra', 'temporal', 'transito'
|
|
);
|
|
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
|
|
|
DO $$ BEGIN
|
|
CREATE TYPE inventory.requisition_status AS ENUM (
|
|
'draft', 'submitted', 'approved', 'partially_served', 'served', 'cancelled'
|
|
);
|
|
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
|
|
|
-- ============================================================================
|
|
-- TABLES - EXTENSIONES CONSTRUCCIÓN
|
|
-- ============================================================================
|
|
|
|
-- Tabla: almacenes_proyecto (almacén por proyecto/obra)
|
|
-- Extiende: inventory.warehouses (ERP Core)
|
|
CREATE TABLE IF NOT EXISTS inventory.almacenes_proyecto (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE,
|
|
warehouse_id UUID NOT NULL, -- FK a inventory.warehouses (ERP Core)
|
|
fraccionamiento_id UUID NOT NULL REFERENCES construction.fraccionamientos(id),
|
|
construction_warehouse_type inventory.warehouse_type_construction NOT NULL DEFAULT 'obra',
|
|
location_description TEXT,
|
|
location GEOMETRY(POINT, 4326),
|
|
responsible_id UUID REFERENCES auth.users(id),
|
|
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
created_by UUID REFERENCES auth.users(id),
|
|
updated_at TIMESTAMPTZ,
|
|
updated_by UUID REFERENCES auth.users(id),
|
|
deleted_at TIMESTAMPTZ,
|
|
deleted_by UUID REFERENCES auth.users(id),
|
|
CONSTRAINT uq_almacenes_proyecto_warehouse UNIQUE (warehouse_id)
|
|
);
|
|
|
|
-- Tabla: requisiciones_obra (requisiciones desde obra)
|
|
CREATE TABLE IF NOT EXISTS inventory.requisiciones_obra (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE,
|
|
fraccionamiento_id UUID NOT NULL REFERENCES construction.fraccionamientos(id),
|
|
requisition_number VARCHAR(30) NOT NULL,
|
|
requisition_date DATE NOT NULL,
|
|
required_date DATE NOT NULL,
|
|
status inventory.requisition_status NOT NULL DEFAULT 'draft',
|
|
priority VARCHAR(20) DEFAULT 'medium',
|
|
requested_by UUID NOT NULL REFERENCES auth.users(id),
|
|
destination_warehouse_id UUID, -- FK a inventory.warehouses (ERP Core)
|
|
approved_by UUID REFERENCES auth.users(id),
|
|
approved_at TIMESTAMPTZ,
|
|
rejection_reason TEXT,
|
|
purchase_order_id UUID, -- FK a purchase.purchase_orders (ERP Core)
|
|
notes TEXT,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
created_by UUID REFERENCES auth.users(id),
|
|
updated_at TIMESTAMPTZ,
|
|
updated_by UUID REFERENCES auth.users(id),
|
|
deleted_at TIMESTAMPTZ,
|
|
deleted_by UUID REFERENCES auth.users(id),
|
|
CONSTRAINT uq_requisiciones_obra_number UNIQUE (tenant_id, requisition_number)
|
|
);
|
|
|
|
-- Tabla: requisicion_lineas (líneas de requisición)
|
|
CREATE TABLE IF NOT EXISTS inventory.requisicion_lineas (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE,
|
|
requisicion_id UUID NOT NULL REFERENCES inventory.requisiciones_obra(id) ON DELETE CASCADE,
|
|
product_id UUID NOT NULL, -- FK a inventory.products (ERP Core)
|
|
concepto_id UUID REFERENCES construction.conceptos(id),
|
|
lote_id UUID REFERENCES construction.lotes(id),
|
|
quantity_requested DECIMAL(12,4) NOT NULL,
|
|
quantity_approved DECIMAL(12,4),
|
|
quantity_served DECIMAL(12,4) DEFAULT 0,
|
|
unit_id UUID, -- FK a core.uom (ERP Core)
|
|
notes TEXT,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
created_by UUID REFERENCES auth.users(id),
|
|
updated_at TIMESTAMPTZ,
|
|
updated_by UUID REFERENCES auth.users(id),
|
|
deleted_at TIMESTAMPTZ,
|
|
deleted_by UUID REFERENCES auth.users(id)
|
|
);
|
|
|
|
-- Tabla: consumos_obra (consumos de materiales por obra/lote)
|
|
CREATE TABLE IF NOT EXISTS inventory.consumos_obra (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE,
|
|
stock_move_id UUID, -- FK a inventory.stock_moves (ERP Core)
|
|
fraccionamiento_id UUID NOT NULL REFERENCES construction.fraccionamientos(id),
|
|
lote_id UUID REFERENCES construction.lotes(id),
|
|
departamento_id UUID REFERENCES construction.departamentos(id),
|
|
concepto_id UUID REFERENCES construction.conceptos(id),
|
|
product_id UUID NOT NULL, -- FK a inventory.products (ERP Core)
|
|
quantity DECIMAL(12,4) NOT NULL,
|
|
unit_cost DECIMAL(12,4),
|
|
total_cost DECIMAL(14,2) GENERATED ALWAYS AS (quantity * unit_cost) STORED,
|
|
consumption_date DATE NOT NULL,
|
|
registered_by UUID NOT NULL REFERENCES auth.users(id),
|
|
notes TEXT,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
created_by UUID REFERENCES auth.users(id),
|
|
updated_at TIMESTAMPTZ,
|
|
updated_by UUID REFERENCES auth.users(id),
|
|
deleted_at TIMESTAMPTZ,
|
|
deleted_by UUID REFERENCES auth.users(id)
|
|
);
|
|
|
|
-- ============================================================================
|
|
-- INDICES
|
|
-- ============================================================================
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_almacenes_proyecto_tenant_id ON inventory.almacenes_proyecto(tenant_id);
|
|
CREATE INDEX IF NOT EXISTS idx_almacenes_proyecto_warehouse_id ON inventory.almacenes_proyecto(warehouse_id);
|
|
CREATE INDEX IF NOT EXISTS idx_almacenes_proyecto_fraccionamiento_id ON inventory.almacenes_proyecto(fraccionamiento_id);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_requisiciones_obra_tenant_id ON inventory.requisiciones_obra(tenant_id);
|
|
CREATE INDEX IF NOT EXISTS idx_requisiciones_obra_fraccionamiento_id ON inventory.requisiciones_obra(fraccionamiento_id);
|
|
CREATE INDEX IF NOT EXISTS idx_requisiciones_obra_status ON inventory.requisiciones_obra(status);
|
|
CREATE INDEX IF NOT EXISTS idx_requisiciones_obra_date ON inventory.requisiciones_obra(requisition_date);
|
|
CREATE INDEX IF NOT EXISTS idx_requisiciones_obra_required_date ON inventory.requisiciones_obra(required_date);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_requisicion_lineas_tenant_id ON inventory.requisicion_lineas(tenant_id);
|
|
CREATE INDEX IF NOT EXISTS idx_requisicion_lineas_requisicion_id ON inventory.requisicion_lineas(requisicion_id);
|
|
CREATE INDEX IF NOT EXISTS idx_requisicion_lineas_product_id ON inventory.requisicion_lineas(product_id);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_consumos_obra_tenant_id ON inventory.consumos_obra(tenant_id);
|
|
CREATE INDEX IF NOT EXISTS idx_consumos_obra_fraccionamiento_id ON inventory.consumos_obra(fraccionamiento_id);
|
|
CREATE INDEX IF NOT EXISTS idx_consumos_obra_lote_id ON inventory.consumos_obra(lote_id);
|
|
CREATE INDEX IF NOT EXISTS idx_consumos_obra_concepto_id ON inventory.consumos_obra(concepto_id);
|
|
CREATE INDEX IF NOT EXISTS idx_consumos_obra_product_id ON inventory.consumos_obra(product_id);
|
|
CREATE INDEX IF NOT EXISTS idx_consumos_obra_date ON inventory.consumos_obra(consumption_date);
|
|
|
|
-- ============================================================================
|
|
-- ROW LEVEL SECURITY (RLS)
|
|
-- ============================================================================
|
|
|
|
ALTER TABLE inventory.almacenes_proyecto ENABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE inventory.requisiciones_obra ENABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE inventory.requisicion_lineas ENABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE inventory.consumos_obra ENABLE ROW LEVEL SECURITY;
|
|
|
|
DO $$ BEGIN
|
|
DROP POLICY IF EXISTS tenant_isolation_almacenes_proyecto ON inventory.almacenes_proyecto;
|
|
CREATE POLICY tenant_isolation_almacenes_proyecto ON inventory.almacenes_proyecto
|
|
FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID);
|
|
EXCEPTION WHEN undefined_object THEN NULL; END $$;
|
|
|
|
DO $$ BEGIN
|
|
DROP POLICY IF EXISTS tenant_isolation_requisiciones_obra ON inventory.requisiciones_obra;
|
|
CREATE POLICY tenant_isolation_requisiciones_obra ON inventory.requisiciones_obra
|
|
FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID);
|
|
EXCEPTION WHEN undefined_object THEN NULL; END $$;
|
|
|
|
DO $$ BEGIN
|
|
DROP POLICY IF EXISTS tenant_isolation_requisicion_lineas ON inventory.requisicion_lineas;
|
|
CREATE POLICY tenant_isolation_requisicion_lineas ON inventory.requisicion_lineas
|
|
FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID);
|
|
EXCEPTION WHEN undefined_object THEN NULL; END $$;
|
|
|
|
DO $$ BEGIN
|
|
DROP POLICY IF EXISTS tenant_isolation_consumos_obra ON inventory.consumos_obra;
|
|
CREATE POLICY tenant_isolation_consumos_obra ON inventory.consumos_obra
|
|
FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID);
|
|
EXCEPTION WHEN undefined_object THEN NULL; END $$;
|
|
|
|
-- ============================================================================
|
|
-- COMENTARIOS
|
|
-- ============================================================================
|
|
|
|
COMMENT ON TABLE inventory.almacenes_proyecto IS 'Extensión: almacenes por proyecto de construcción';
|
|
COMMENT ON TABLE inventory.requisiciones_obra IS 'Extensión: requisiciones de material desde obra';
|
|
COMMENT ON TABLE inventory.requisicion_lineas IS 'Extensión: líneas de requisición de obra';
|
|
COMMENT ON TABLE inventory.consumos_obra IS 'Extensión: consumos de materiales por obra/lote';
|
|
|
|
-- ============================================================================
|
|
-- FK CONSTRAINTS A TABLAS ERP-CORE (Condicionales)
|
|
-- NOTA: Estos constraints solo se crean si las tablas del ERP-Core existen.
|
|
-- Si ERP-Core no está instalado, los campos quedan sin FK formal.
|
|
-- ============================================================================
|
|
|
|
-- FK: almacenes_proyecto.warehouse_id → inventory.warehouses
|
|
DO $$
|
|
BEGIN
|
|
IF EXISTS (SELECT 1 FROM pg_tables WHERE schemaname = 'inventory' AND tablename = 'warehouses') THEN
|
|
IF NOT EXISTS (
|
|
SELECT 1 FROM pg_constraint WHERE conname = 'fk_almacenes_proyecto_warehouse'
|
|
) THEN
|
|
ALTER TABLE inventory.almacenes_proyecto
|
|
ADD CONSTRAINT fk_almacenes_proyecto_warehouse
|
|
FOREIGN KEY (warehouse_id) REFERENCES inventory.warehouses(id)
|
|
ON DELETE RESTRICT;
|
|
RAISE NOTICE 'FK creada: almacenes_proyecto.warehouse_id → inventory.warehouses';
|
|
END IF;
|
|
ELSE
|
|
RAISE NOTICE 'AVISO: inventory.warehouses no existe. FK warehouse_id pendiente.';
|
|
END IF;
|
|
END $$;
|
|
|
|
-- FK: requisicion_lineas.product_id → products.products
|
|
DO $$
|
|
BEGIN
|
|
IF EXISTS (SELECT 1 FROM pg_tables WHERE schemaname = 'products' AND tablename = 'products') THEN
|
|
IF NOT EXISTS (
|
|
SELECT 1 FROM pg_constraint WHERE conname = 'fk_requisicion_lineas_product'
|
|
) THEN
|
|
ALTER TABLE inventory.requisicion_lineas
|
|
ADD CONSTRAINT fk_requisicion_lineas_product
|
|
FOREIGN KEY (product_id) REFERENCES products.products(id)
|
|
ON DELETE RESTRICT;
|
|
RAISE NOTICE 'FK creada: requisicion_lineas.product_id → products.products';
|
|
END IF;
|
|
ELSE
|
|
RAISE NOTICE 'AVISO: products.products no existe. FK product_id pendiente.';
|
|
END IF;
|
|
END $$;
|
|
|
|
-- FK: requisicion_lineas.unit_id → core.uom (opcional porque es NULLABLE)
|
|
DO $$
|
|
BEGIN
|
|
IF EXISTS (SELECT 1 FROM pg_tables WHERE schemaname = 'core' AND tablename = 'uom') THEN
|
|
IF NOT EXISTS (
|
|
SELECT 1 FROM pg_constraint WHERE conname = 'fk_requisicion_lineas_unit'
|
|
) THEN
|
|
ALTER TABLE inventory.requisicion_lineas
|
|
ADD CONSTRAINT fk_requisicion_lineas_unit
|
|
FOREIGN KEY (unit_id) REFERENCES core.uom(id)
|
|
ON DELETE SET NULL;
|
|
RAISE NOTICE 'FK creada: requisicion_lineas.unit_id → core.uom';
|
|
END IF;
|
|
ELSE
|
|
RAISE NOTICE 'AVISO: core.uom no existe. FK unit_id pendiente.';
|
|
END IF;
|
|
END $$;
|
|
|
|
-- FK: requisiciones_obra.destination_warehouse_id → inventory.warehouses
|
|
DO $$
|
|
BEGIN
|
|
IF EXISTS (SELECT 1 FROM pg_tables WHERE schemaname = 'inventory' AND tablename = 'warehouses') THEN
|
|
IF NOT EXISTS (
|
|
SELECT 1 FROM pg_constraint WHERE conname = 'fk_requisiciones_obra_dest_warehouse'
|
|
) THEN
|
|
ALTER TABLE inventory.requisiciones_obra
|
|
ADD CONSTRAINT fk_requisiciones_obra_dest_warehouse
|
|
FOREIGN KEY (destination_warehouse_id) REFERENCES inventory.warehouses(id)
|
|
ON DELETE SET NULL;
|
|
RAISE NOTICE 'FK creada: requisiciones_obra.destination_warehouse_id → inventory.warehouses';
|
|
END IF;
|
|
END IF;
|
|
END $$;
|
|
|
|
-- FK: requisiciones_obra.purchase_order_id → purchases.purchase_orders
|
|
DO $$
|
|
BEGIN
|
|
IF EXISTS (SELECT 1 FROM pg_tables WHERE schemaname = 'purchases' AND tablename = 'purchase_orders') THEN
|
|
IF NOT EXISTS (
|
|
SELECT 1 FROM pg_constraint WHERE conname = 'fk_requisiciones_obra_purchase_order'
|
|
) THEN
|
|
ALTER TABLE inventory.requisiciones_obra
|
|
ADD CONSTRAINT fk_requisiciones_obra_purchase_order
|
|
FOREIGN KEY (purchase_order_id) REFERENCES purchases.purchase_orders(id)
|
|
ON DELETE SET NULL;
|
|
RAISE NOTICE 'FK creada: requisiciones_obra.purchase_order_id → purchases.purchase_orders';
|
|
END IF;
|
|
END IF;
|
|
END $$;
|
|
|
|
-- FK: consumos_obra.product_id → products.products
|
|
DO $$
|
|
BEGIN
|
|
IF EXISTS (SELECT 1 FROM pg_tables WHERE schemaname = 'products' AND tablename = 'products') THEN
|
|
IF NOT EXISTS (
|
|
SELECT 1 FROM pg_constraint WHERE conname = 'fk_consumos_obra_product'
|
|
) THEN
|
|
ALTER TABLE inventory.consumos_obra
|
|
ADD CONSTRAINT fk_consumos_obra_product
|
|
FOREIGN KEY (product_id) REFERENCES products.products(id)
|
|
ON DELETE RESTRICT;
|
|
RAISE NOTICE 'FK creada: consumos_obra.product_id → products.products';
|
|
END IF;
|
|
END IF;
|
|
END $$;
|
|
|
|
-- FK: consumos_obra.stock_move_id → inventory.stock_moves
|
|
DO $$
|
|
BEGIN
|
|
IF EXISTS (SELECT 1 FROM pg_tables WHERE schemaname = 'inventory' AND tablename = 'stock_moves') THEN
|
|
IF NOT EXISTS (
|
|
SELECT 1 FROM pg_constraint WHERE conname = 'fk_consumos_obra_stock_move'
|
|
) THEN
|
|
ALTER TABLE inventory.consumos_obra
|
|
ADD CONSTRAINT fk_consumos_obra_stock_move
|
|
FOREIGN KEY (stock_move_id) REFERENCES inventory.stock_moves(id)
|
|
ON DELETE SET NULL;
|
|
RAISE NOTICE 'FK creada: consumos_obra.stock_move_id → inventory.stock_moves';
|
|
END IF;
|
|
END IF;
|
|
END $$;
|
|
|
|
-- ============================================================================
|
|
-- FIN DE EXTENSIONES INVENTORY
|
|
-- Total tablas: 4
|
|
-- FK constraints condicionales: 7
|
|
-- ============================================================================
|