-- ===================================================== -- SCHEMA: inventory -- PROPÓSITO: Gestión de inventarios, productos, almacenes, movimientos -- MÓDULOS: MGN-005 (Inventario Básico) -- FECHA: 2025-11-24 -- ===================================================== -- Crear schema CREATE SCHEMA IF NOT EXISTS inventory; -- ===================================================== -- TYPES (ENUMs) -- ===================================================== CREATE TYPE inventory.product_type AS ENUM ( 'storable', 'consumable', 'service' ); CREATE TYPE inventory.tracking_type AS ENUM ( 'none', 'lot', 'serial' ); CREATE TYPE inventory.location_type AS ENUM ( 'internal', 'customer', 'supplier', 'inventory', 'production', 'transit' ); CREATE TYPE inventory.picking_type AS ENUM ( 'incoming', 'outgoing', 'internal' ); CREATE TYPE inventory.move_status AS ENUM ( 'draft', 'waiting', -- COR-002: Esperando disponibilidad (Odoo alignment) 'confirmed', 'partially_available', -- COR-002: Parcialmente disponible (Odoo alignment) 'assigned', 'done', 'cancelled' ); CREATE TYPE inventory.valuation_method AS ENUM ( 'fifo', 'average', 'standard' ); -- ===================================================== -- TABLES -- ===================================================== -- Tabla: products (Productos) CREATE TABLE inventory.products ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, -- Identificación name VARCHAR(255) NOT NULL, code VARCHAR(100), barcode VARCHAR(100), description TEXT, -- Tipo product_type inventory.product_type NOT NULL DEFAULT 'storable', tracking inventory.tracking_type NOT NULL DEFAULT 'none', -- Categoría category_id UUID REFERENCES core.product_categories(id), -- Unidades de medida uom_id UUID NOT NULL REFERENCES core.uom(id), -- UoM de venta/uso purchase_uom_id UUID REFERENCES core.uom(id), -- UoM de compra -- Precios cost_price DECIMAL(15, 4) DEFAULT 0, list_price DECIMAL(15, 4) DEFAULT 0, -- Configuración de inventario valuation_method inventory.valuation_method DEFAULT 'fifo', is_storable BOOLEAN GENERATED ALWAYS AS (product_type = 'storable') STORED, -- Pesos y dimensiones weight DECIMAL(12, 4), volume DECIMAL(12, 4), -- Proveedores y clientes can_be_sold BOOLEAN DEFAULT TRUE, can_be_purchased BOOLEAN DEFAULT TRUE, -- Imagen image_url VARCHAR(500), -- Control active BOOLEAN NOT NULL 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), deleted_at TIMESTAMP, deleted_by UUID REFERENCES auth.users(id), CONSTRAINT uq_products_code_tenant UNIQUE (tenant_id, code), CONSTRAINT uq_products_barcode UNIQUE (barcode) ); -- Tabla: product_variants (Variantes de producto) CREATE TABLE inventory.product_variants ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), product_template_id UUID NOT NULL REFERENCES inventory.products(id) ON DELETE CASCADE, -- Atributos (JSON) -- Ejemplo: {"color": "red", "size": "XL"} attribute_values JSONB NOT NULL DEFAULT '{}', -- Identificación name VARCHAR(255), code VARCHAR(100), barcode VARCHAR(100), -- Precio diferencial price_extra DECIMAL(15, 4) DEFAULT 0, -- Control active BOOLEAN NOT NULL DEFAULT TRUE, -- Auditoría created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, created_by UUID REFERENCES auth.users(id), CONSTRAINT uq_product_variants_barcode UNIQUE (barcode) ); -- Tabla: warehouses (Almacenes) CREATE TABLE inventory.warehouses ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, company_id UUID NOT NULL REFERENCES auth.companies(id) ON DELETE CASCADE, name VARCHAR(255) NOT NULL, code VARCHAR(20) NOT NULL, -- Dirección address_id UUID REFERENCES core.addresses(id), -- Configuración is_default BOOLEAN DEFAULT FALSE, -- Control active BOOLEAN NOT NULL 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_warehouses_code_company UNIQUE (company_id, code) ); -- Tabla: locations (Ubicaciones de inventario) CREATE TABLE inventory.locations ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, warehouse_id UUID REFERENCES inventory.warehouses(id), name VARCHAR(255) NOT NULL, complete_name TEXT, -- Generado: "Warehouse / Zone A / Shelf 1" location_type inventory.location_type NOT NULL DEFAULT 'internal', -- Jerarquía parent_id UUID REFERENCES inventory.locations(id), -- Configuración is_scrap_location BOOLEAN DEFAULT FALSE, is_return_location BOOLEAN DEFAULT FALSE, -- Control active BOOLEAN NOT NULL 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 chk_locations_no_self_parent CHECK (id != parent_id) ); -- Tabla: lots (Lotes/Series) - DEBE IR ANTES DE stock_quants por FK CREATE TABLE inventory.lots ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, product_id UUID NOT NULL REFERENCES inventory.products(id), name VARCHAR(100) NOT NULL, ref VARCHAR(100), -- Referencia externa -- Fechas manufacture_date DATE, expiration_date DATE, removal_date DATE, alert_date DATE, -- Notas notes TEXT, -- Auditoría created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, created_by UUID REFERENCES auth.users(id), CONSTRAINT uq_lots_name_product UNIQUE (product_id, name), CONSTRAINT chk_lots_expiration CHECK (expiration_date IS NULL OR expiration_date > manufacture_date) ); -- Tabla: stock_quants (Cantidades en stock) CREATE TABLE inventory.stock_quants ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, product_id UUID NOT NULL REFERENCES inventory.products(id), location_id UUID NOT NULL REFERENCES inventory.locations(id), lot_id UUID REFERENCES inventory.lots(id), -- Cantidades quantity DECIMAL(12, 4) NOT NULL DEFAULT 0, reserved_quantity DECIMAL(12, 4) NOT NULL DEFAULT 0, available_quantity DECIMAL(12, 4) GENERATED ALWAYS AS (quantity - reserved_quantity) STORED, -- Valoración cost DECIMAL(15, 4) DEFAULT 0, -- Auditoría created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP, CONSTRAINT chk_stock_quants_reserved CHECK (reserved_quantity >= 0 AND reserved_quantity <= quantity) ); -- Unique index for stock_quants (allows expressions unlike UNIQUE constraint) CREATE UNIQUE INDEX uq_stock_quants_product_location_lot ON inventory.stock_quants (tenant_id, product_id, location_id, COALESCE(lot_id, '00000000-0000-0000-0000-000000000000'::UUID)); -- Índices para stock_quants CREATE INDEX idx_stock_quants_tenant_id ON inventory.stock_quants(tenant_id); CREATE INDEX idx_stock_quants_product_location ON inventory.stock_quants(product_id, location_id); -- RLS para stock_quants ALTER TABLE inventory.stock_quants ENABLE ROW LEVEL SECURITY; CREATE POLICY tenant_isolation_stock_quants ON inventory.stock_quants USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid); -- Tabla: pickings (Albaranes/Transferencias) CREATE TABLE inventory.pickings ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, company_id UUID NOT NULL REFERENCES auth.companies(id) ON DELETE CASCADE, name VARCHAR(100) NOT NULL, picking_type inventory.picking_type NOT NULL, -- COR-007: Tipo de operación (referencia a picking_types) picking_type_id UUID, -- FK agregada después de crear picking_types -- Ubicaciones location_id UUID NOT NULL REFERENCES inventory.locations(id), -- Origen location_dest_id UUID NOT NULL REFERENCES inventory.locations(id), -- Destino -- Partner (cliente/proveedor) partner_id UUID REFERENCES core.partners(id), -- Fechas scheduled_date TIMESTAMP, date_done TIMESTAMP, -- Origen origin VARCHAR(255), -- Referencia al documento origen (PO, SO, etc.) -- COR-018: Backorder support backorder_id UUID, -- FK a picking padre si es backorder -- Estado status inventory.move_status NOT NULL DEFAULT 'draft', -- Notas notes TEXT, -- 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), validated_at TIMESTAMP, validated_by UUID REFERENCES auth.users(id), CONSTRAINT uq_pickings_name_company UNIQUE (company_id, name) ); -- Tabla: stock_moves (Movimientos de inventario) CREATE TABLE inventory.stock_moves ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, product_id UUID NOT NULL REFERENCES inventory.products(id), product_uom_id UUID NOT NULL REFERENCES core.uom(id), -- Ubicaciones location_id UUID NOT NULL REFERENCES inventory.locations(id), -- Origen location_dest_id UUID NOT NULL REFERENCES inventory.locations(id), -- Destino -- Cantidades product_qty DECIMAL(12, 4) NOT NULL, quantity_done DECIMAL(12, 4) DEFAULT 0, -- Lote/Serie lot_id UUID REFERENCES inventory.lots(id), -- Relación con picking picking_id UUID REFERENCES inventory.pickings(id) ON DELETE CASCADE, -- Origen del movimiento origin VARCHAR(255), ref VARCHAR(255), -- Estado status inventory.move_status NOT NULL DEFAULT 'draft', -- Fechas date_expected TIMESTAMP, date TIMESTAMP, -- Precio (para valoración) price_unit DECIMAL(15, 4) DEFAULT 0, -- Analítica analytic_account_id UUID REFERENCES analytics.analytic_accounts(id), -- Distribución analítica -- 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 chk_stock_moves_quantity CHECK (product_qty > 0), CONSTRAINT chk_stock_moves_quantity_done CHECK (quantity_done >= 0) ); -- ===================================================== -- COR-003: Tabla stock_move_lines (Líneas de movimiento) -- Granularidad a nivel lote/serie (equivalente a stock.move.line Odoo) -- ===================================================== CREATE TABLE inventory.stock_move_lines ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, -- Relación con move move_id UUID NOT NULL REFERENCES inventory.stock_moves(id) ON DELETE CASCADE, -- Producto product_id UUID NOT NULL REFERENCES inventory.products(id), product_uom_id UUID NOT NULL REFERENCES core.uom(id), -- Lote/Serie/Paquete lot_id UUID REFERENCES inventory.lots(id), package_id UUID, -- Futuro: packages table result_package_id UUID, -- Futuro: packages table owner_id UUID REFERENCES core.partners(id), -- Ubicaciones location_id UUID NOT NULL REFERENCES inventory.locations(id), location_dest_id UUID NOT NULL REFERENCES inventory.locations(id), -- Cantidades quantity DECIMAL(12, 4) NOT NULL, quantity_done DECIMAL(12, 4) DEFAULT 0, -- Estado state VARCHAR(20), -- Fechas date TIMESTAMP, -- Referencia reference VARCHAR(255), -- Auditoría created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, created_by UUID REFERENCES auth.users(id), updated_at TIMESTAMP, CONSTRAINT chk_move_lines_qty CHECK (quantity > 0), CONSTRAINT chk_move_lines_qty_done CHECK (quantity_done >= 0 AND quantity_done <= quantity) ); COMMENT ON TABLE inventory.stock_move_lines IS 'COR-003: Líneas de movimiento de stock para granularidad a nivel lote/serie (equivalente a stock.move.line Odoo)'; -- ===================================================== -- COR-007: Tabla picking_types (Tipos de operación) -- Configuración de operaciones de almacén (equivalente a stock.picking.type Odoo) -- ===================================================== CREATE TABLE inventory.picking_types ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, warehouse_id UUID REFERENCES inventory.warehouses(id), name VARCHAR(100) NOT NULL, code VARCHAR(20) NOT NULL, -- incoming, outgoing, internal sequence INTEGER DEFAULT 10, -- Secuencia de numeración sequence_id UUID REFERENCES core.sequences(id), -- Ubicaciones por defecto default_location_src_id UUID REFERENCES inventory.locations(id), default_location_dest_id UUID REFERENCES inventory.locations(id), -- Tipo de retorno return_picking_type_id UUID REFERENCES inventory.picking_types(id), -- Configuración show_operations BOOLEAN DEFAULT FALSE, show_reserved BOOLEAN DEFAULT TRUE, use_create_lots BOOLEAN DEFAULT FALSE, use_existing_lots BOOLEAN DEFAULT TRUE, print_label BOOLEAN DEFAULT FALSE, -- Control active BOOLEAN NOT NULL 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_picking_types_code_warehouse UNIQUE (warehouse_id, code) ); COMMENT ON TABLE inventory.picking_types IS 'COR-007: Tipos de operación de almacén (equivalente a stock.picking.type Odoo)'; -- ===================================================== -- COR-008: Tablas de Atributos de Producto -- Sistema de variantes (equivalente a product.attribute Odoo) -- ===================================================== -- Tabla: product_attributes (Atributos) CREATE TABLE inventory.product_attributes ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, name VARCHAR(100) NOT NULL, sequence INTEGER DEFAULT 10, -- Configuración de variantes create_variant VARCHAR(20) DEFAULT 'always', -- always, dynamic, no_variant display_type VARCHAR(20) DEFAULT 'radio', -- radio, select, color, multi -- Auditoría created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, created_by UUID REFERENCES auth.users(id), CONSTRAINT uq_product_attributes_name_tenant UNIQUE (tenant_id, name), CONSTRAINT chk_product_attributes_create_variant CHECK (create_variant IN ('always', 'dynamic', 'no_variant')), CONSTRAINT chk_product_attributes_display_type CHECK (display_type IN ('radio', 'select', 'color', 'multi')) ); -- Tabla: product_attribute_values (Valores de atributos) CREATE TABLE inventory.product_attribute_values ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, attribute_id UUID NOT NULL REFERENCES inventory.product_attributes(id) ON DELETE CASCADE, name VARCHAR(100) NOT NULL, sequence INTEGER DEFAULT 10, html_color VARCHAR(10), -- Para display_type='color' is_custom BOOLEAN DEFAULT FALSE, -- Auditoría created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, created_by UUID REFERENCES auth.users(id), CONSTRAINT uq_product_attribute_values_name UNIQUE (attribute_id, name) ); -- Tabla: product_template_attribute_lines (Líneas de atributo por producto) CREATE TABLE inventory.product_template_attribute_lines ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, product_tmpl_id UUID NOT NULL REFERENCES inventory.products(id) ON DELETE CASCADE, attribute_id UUID NOT NULL REFERENCES inventory.product_attributes(id), value_ids UUID[] NOT NULL, -- Array de product_attribute_value ids -- Auditoría created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, created_by UUID REFERENCES auth.users(id), CONSTRAINT uq_ptal_product_attribute UNIQUE (product_tmpl_id, attribute_id) ); -- Tabla: product_template_attribute_values (Valores por template) CREATE TABLE inventory.product_template_attribute_values ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, attribute_line_id UUID NOT NULL REFERENCES inventory.product_template_attribute_lines(id) ON DELETE CASCADE, product_attribute_value_id UUID NOT NULL REFERENCES inventory.product_attribute_values(id), -- Precio extra price_extra DECIMAL(15, 4) DEFAULT 0, -- Exclusión ptav_active BOOLEAN DEFAULT TRUE, -- Auditoría created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ); COMMENT ON TABLE inventory.product_attributes IS 'COR-008: Atributos de producto (color, talla, etc.) - equivalente a product.attribute Odoo'; COMMENT ON TABLE inventory.product_attribute_values IS 'COR-008: Valores posibles para cada atributo - equivalente a product.attribute.value Odoo'; COMMENT ON TABLE inventory.product_template_attribute_lines IS 'COR-008: Líneas de atributo por plantilla de producto - equivalente a product.template.attribute.line Odoo'; COMMENT ON TABLE inventory.product_template_attribute_values IS 'COR-008: Valores de atributo aplicados a plantilla - equivalente a product.template.attribute.value Odoo'; -- Tabla: inventory_adjustments (Ajustes de inventario) CREATE TABLE inventory.inventory_adjustments ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, company_id UUID NOT NULL REFERENCES auth.companies(id) ON DELETE CASCADE, name VARCHAR(100) NOT NULL, -- Ubicación a ajustar location_id UUID NOT NULL REFERENCES inventory.locations(id), -- Fecha de conteo date DATE NOT NULL, -- Estado status inventory.move_status NOT NULL DEFAULT 'draft', -- Notas notes TEXT, -- 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), validated_at TIMESTAMP, validated_by UUID REFERENCES auth.users(id), CONSTRAINT uq_inventory_adjustments_name_company UNIQUE (company_id, name) ); -- Tabla: inventory_adjustment_lines (Líneas de ajuste) CREATE TABLE inventory.inventory_adjustment_lines ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, adjustment_id UUID NOT NULL REFERENCES inventory.inventory_adjustments(id) ON DELETE CASCADE, product_id UUID NOT NULL REFERENCES inventory.products(id), location_id UUID NOT NULL REFERENCES inventory.locations(id), lot_id UUID REFERENCES inventory.lots(id), -- Cantidades theoretical_qty DECIMAL(12, 4) NOT NULL DEFAULT 0, -- Cantidad teórica del sistema counted_qty DECIMAL(12, 4) NOT NULL, -- Cantidad contada físicamente difference_qty DECIMAL(12, 4) GENERATED ALWAYS AS (counted_qty - theoretical_qty) STORED, -- Auditoría created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ); -- Índices para inventory_adjustment_lines CREATE INDEX idx_inventory_adjustment_lines_tenant_id ON inventory.inventory_adjustment_lines(tenant_id); -- RLS para inventory_adjustment_lines ALTER TABLE inventory.inventory_adjustment_lines ENABLE ROW LEVEL SECURITY; CREATE POLICY tenant_isolation_inventory_adjustment_lines ON inventory.inventory_adjustment_lines USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid); -- ===================================================== -- INDICES -- ===================================================== -- Products CREATE INDEX idx_products_tenant_id ON inventory.products(tenant_id); CREATE INDEX idx_products_code ON inventory.products(code); CREATE INDEX idx_products_barcode ON inventory.products(barcode); CREATE INDEX idx_products_category_id ON inventory.products(category_id); CREATE INDEX idx_products_type ON inventory.products(product_type); CREATE INDEX idx_products_active ON inventory.products(active) WHERE active = TRUE; -- Product Variants CREATE INDEX idx_product_variants_template_id ON inventory.product_variants(product_template_id); CREATE INDEX idx_product_variants_barcode ON inventory.product_variants(barcode); -- Warehouses CREATE INDEX idx_warehouses_tenant_id ON inventory.warehouses(tenant_id); CREATE INDEX idx_warehouses_company_id ON inventory.warehouses(company_id); CREATE INDEX idx_warehouses_code ON inventory.warehouses(code); -- Locations CREATE INDEX idx_locations_tenant_id ON inventory.locations(tenant_id); CREATE INDEX idx_locations_warehouse_id ON inventory.locations(warehouse_id); CREATE INDEX idx_locations_parent_id ON inventory.locations(parent_id); CREATE INDEX idx_locations_type ON inventory.locations(location_type); -- Stock Quants CREATE INDEX idx_stock_quants_product_id ON inventory.stock_quants(product_id); CREATE INDEX idx_stock_quants_location_id ON inventory.stock_quants(location_id); CREATE INDEX idx_stock_quants_lot_id ON inventory.stock_quants(lot_id); CREATE INDEX idx_stock_quants_available ON inventory.stock_quants(product_id, location_id) WHERE available_quantity > 0; -- Lots CREATE INDEX idx_lots_tenant_id ON inventory.lots(tenant_id); CREATE INDEX idx_lots_product_id ON inventory.lots(product_id); CREATE INDEX idx_lots_name ON inventory.lots(name); CREATE INDEX idx_lots_expiration_date ON inventory.lots(expiration_date); -- Pickings CREATE INDEX idx_pickings_tenant_id ON inventory.pickings(tenant_id); CREATE INDEX idx_pickings_company_id ON inventory.pickings(company_id); CREATE INDEX idx_pickings_name ON inventory.pickings(name); CREATE INDEX idx_pickings_type ON inventory.pickings(picking_type); CREATE INDEX idx_pickings_status ON inventory.pickings(status); CREATE INDEX idx_pickings_partner_id ON inventory.pickings(partner_id); CREATE INDEX idx_pickings_origin ON inventory.pickings(origin); CREATE INDEX idx_pickings_scheduled_date ON inventory.pickings(scheduled_date); -- Stock Moves CREATE INDEX idx_stock_moves_tenant_id ON inventory.stock_moves(tenant_id); CREATE INDEX idx_stock_moves_product_id ON inventory.stock_moves(product_id); CREATE INDEX idx_stock_moves_picking_id ON inventory.stock_moves(picking_id); CREATE INDEX idx_stock_moves_location_id ON inventory.stock_moves(location_id); CREATE INDEX idx_stock_moves_location_dest_id ON inventory.stock_moves(location_dest_id); CREATE INDEX idx_stock_moves_status ON inventory.stock_moves(status); CREATE INDEX idx_stock_moves_lot_id ON inventory.stock_moves(lot_id); CREATE INDEX idx_stock_moves_analytic_account_id ON inventory.stock_moves(analytic_account_id) WHERE analytic_account_id IS NOT NULL; -- Inventory Adjustments CREATE INDEX idx_inventory_adjustments_tenant_id ON inventory.inventory_adjustments(tenant_id); CREATE INDEX idx_inventory_adjustments_company_id ON inventory.inventory_adjustments(company_id); CREATE INDEX idx_inventory_adjustments_location_id ON inventory.inventory_adjustments(location_id); CREATE INDEX idx_inventory_adjustments_status ON inventory.inventory_adjustments(status); CREATE INDEX idx_inventory_adjustments_date ON inventory.inventory_adjustments(date); -- Inventory Adjustment Lines CREATE INDEX idx_inventory_adjustment_lines_adjustment_id ON inventory.inventory_adjustment_lines(adjustment_id); CREATE INDEX idx_inventory_adjustment_lines_product_id ON inventory.inventory_adjustment_lines(product_id); -- ===================================================== -- FUNCTIONS -- ===================================================== -- Función: update_stock_quant -- Actualiza la cantidad en stock de un producto en una ubicación CREATE OR REPLACE FUNCTION inventory.update_stock_quant( p_product_id UUID, p_location_id UUID, p_lot_id UUID, p_quantity DECIMAL ) RETURNS VOID AS $$ BEGIN INSERT INTO inventory.stock_quants (product_id, location_id, lot_id, quantity) VALUES (p_product_id, p_location_id, p_lot_id, p_quantity) ON CONFLICT (product_id, location_id, COALESCE(lot_id, '00000000-0000-0000-0000-000000000000'::UUID)) DO UPDATE SET quantity = inventory.stock_quants.quantity + EXCLUDED.quantity, updated_at = CURRENT_TIMESTAMP; END; $$ LANGUAGE plpgsql; COMMENT ON FUNCTION inventory.update_stock_quant IS 'Actualiza la cantidad en stock de un producto en una ubicación'; -- Función: reserve_quantity -- Reserva cantidad de un producto en una ubicación CREATE OR REPLACE FUNCTION inventory.reserve_quantity( p_product_id UUID, p_location_id UUID, p_lot_id UUID, p_quantity DECIMAL ) RETURNS BOOLEAN AS $$ DECLARE v_available DECIMAL; BEGIN -- Verificar disponibilidad SELECT available_quantity INTO v_available FROM inventory.stock_quants WHERE product_id = p_product_id AND location_id = p_location_id AND (lot_id = p_lot_id OR (lot_id IS NULL AND p_lot_id IS NULL)); IF v_available IS NULL OR v_available < p_quantity THEN RETURN FALSE; END IF; -- Reservar UPDATE inventory.stock_quants SET reserved_quantity = reserved_quantity + p_quantity, updated_at = CURRENT_TIMESTAMP WHERE product_id = p_product_id AND location_id = p_location_id AND (lot_id = p_lot_id OR (lot_id IS NULL AND p_lot_id IS NULL)); RETURN TRUE; END; $$ LANGUAGE plpgsql; COMMENT ON FUNCTION inventory.reserve_quantity IS 'Reserva cantidad de un producto en una ubicación'; -- Función: get_product_stock -- Obtiene el stock disponible de un producto CREATE OR REPLACE FUNCTION inventory.get_product_stock( p_product_id UUID, p_location_id UUID DEFAULT NULL ) RETURNS TABLE( location_id UUID, location_name VARCHAR, quantity DECIMAL, reserved_quantity DECIMAL, available_quantity DECIMAL ) AS $$ BEGIN RETURN QUERY SELECT sq.location_id, l.name AS location_name, sq.quantity, sq.reserved_quantity, sq.available_quantity FROM inventory.stock_quants sq JOIN inventory.locations l ON sq.location_id = l.id WHERE sq.product_id = p_product_id AND (p_location_id IS NULL OR sq.location_id = p_location_id) AND sq.quantity > 0; END; $$ LANGUAGE plpgsql STABLE; COMMENT ON FUNCTION inventory.get_product_stock IS 'Obtiene el stock disponible de un producto por ubicación'; -- Función: process_stock_move -- Procesa un movimiento de inventario (actualiza quants) CREATE OR REPLACE FUNCTION inventory.process_stock_move(p_move_id UUID) RETURNS VOID AS $$ DECLARE v_move RECORD; BEGIN -- Obtener datos del movimiento SELECT * INTO v_move FROM inventory.stock_moves WHERE id = p_move_id; IF NOT FOUND THEN RAISE EXCEPTION 'Stock move % not found', p_move_id; END IF; IF v_move.status != 'confirmed' THEN RAISE EXCEPTION 'Stock move % is not in confirmed status', p_move_id; END IF; -- Decrementar en ubicación origen PERFORM inventory.update_stock_quant( v_move.product_id, v_move.location_id, v_move.lot_id, -v_move.quantity_done ); -- Incrementar en ubicación destino PERFORM inventory.update_stock_quant( v_move.product_id, v_move.location_dest_id, v_move.lot_id, v_move.quantity_done ); -- Actualizar estado del movimiento UPDATE inventory.stock_moves SET status = 'done', date = CURRENT_TIMESTAMP, updated_at = CURRENT_TIMESTAMP, updated_by = get_current_user_id() WHERE id = p_move_id; END; $$ LANGUAGE plpgsql; COMMENT ON FUNCTION inventory.process_stock_move IS 'Procesa un movimiento de inventario y actualiza los quants'; -- Función: update_location_complete_name -- Actualiza el nombre completo de una ubicación CREATE OR REPLACE FUNCTION inventory.update_location_complete_name() RETURNS TRIGGER AS $$ DECLARE v_parent_name TEXT; BEGIN IF NEW.parent_id IS NULL THEN NEW.complete_name := NEW.name; ELSE SELECT complete_name INTO v_parent_name FROM inventory.locations WHERE id = NEW.parent_id; NEW.complete_name := v_parent_name || ' / ' || NEW.name; END IF; RETURN NEW; END; $$ LANGUAGE plpgsql; COMMENT ON FUNCTION inventory.update_location_complete_name IS 'Actualiza el nombre completo de la ubicación'; -- ===================================================== -- TRIGGERS -- ===================================================== -- Trigger: Actualizar updated_at CREATE TRIGGER trg_products_updated_at BEFORE UPDATE ON inventory.products FOR EACH ROW EXECUTE FUNCTION auth.update_updated_at_column(); CREATE TRIGGER trg_warehouses_updated_at BEFORE UPDATE ON inventory.warehouses FOR EACH ROW EXECUTE FUNCTION auth.update_updated_at_column(); CREATE TRIGGER trg_locations_updated_at BEFORE UPDATE ON inventory.locations FOR EACH ROW EXECUTE FUNCTION auth.update_updated_at_column(); CREATE TRIGGER trg_pickings_updated_at BEFORE UPDATE ON inventory.pickings FOR EACH ROW EXECUTE FUNCTION auth.update_updated_at_column(); CREATE TRIGGER trg_stock_moves_updated_at BEFORE UPDATE ON inventory.stock_moves FOR EACH ROW EXECUTE FUNCTION auth.update_updated_at_column(); CREATE TRIGGER trg_inventory_adjustments_updated_at BEFORE UPDATE ON inventory.inventory_adjustments FOR EACH ROW EXECUTE FUNCTION auth.update_updated_at_column(); -- Trigger: Actualizar complete_name de ubicación CREATE TRIGGER trg_locations_update_complete_name BEFORE INSERT OR UPDATE OF name, parent_id ON inventory.locations FOR EACH ROW EXECUTE FUNCTION inventory.update_location_complete_name(); -- ===================================================== -- TRACKING AUTOMÁTICO (mail.thread pattern) -- ===================================================== -- Trigger: Tracking automático para movimientos de stock CREATE TRIGGER track_stock_move_changes AFTER INSERT OR UPDATE OR DELETE ON inventory.stock_moves FOR EACH ROW EXECUTE FUNCTION system.track_field_changes(); COMMENT ON TRIGGER track_stock_move_changes ON inventory.stock_moves IS 'Registra automáticamente cambios en movimientos de stock (estado, producto, cantidad, ubicaciones)'; -- ===================================================== -- ROW LEVEL SECURITY (RLS) -- ===================================================== ALTER TABLE inventory.products ENABLE ROW LEVEL SECURITY; ALTER TABLE inventory.warehouses ENABLE ROW LEVEL SECURITY; ALTER TABLE inventory.locations ENABLE ROW LEVEL SECURITY; ALTER TABLE inventory.lots ENABLE ROW LEVEL SECURITY; ALTER TABLE inventory.pickings ENABLE ROW LEVEL SECURITY; ALTER TABLE inventory.stock_moves ENABLE ROW LEVEL SECURITY; ALTER TABLE inventory.inventory_adjustments ENABLE ROW LEVEL SECURITY; CREATE POLICY tenant_isolation_products ON inventory.products USING (tenant_id = get_current_tenant_id()); CREATE POLICY tenant_isolation_warehouses ON inventory.warehouses USING (tenant_id = get_current_tenant_id()); CREATE POLICY tenant_isolation_locations ON inventory.locations USING (tenant_id = get_current_tenant_id()); CREATE POLICY tenant_isolation_lots ON inventory.lots USING (tenant_id = get_current_tenant_id()); CREATE POLICY tenant_isolation_pickings ON inventory.pickings USING (tenant_id = get_current_tenant_id()); CREATE POLICY tenant_isolation_stock_moves ON inventory.stock_moves USING (tenant_id = get_current_tenant_id()); CREATE POLICY tenant_isolation_inventory_adjustments ON inventory.inventory_adjustments USING (tenant_id = get_current_tenant_id()); -- ===================================================== -- COMENTARIOS -- ===================================================== COMMENT ON SCHEMA inventory IS 'Schema de gestión de inventarios, productos, almacenes y movimientos'; COMMENT ON TABLE inventory.products IS 'Productos (almacenables, consumibles, servicios)'; COMMENT ON TABLE inventory.product_variants IS 'Variantes de productos (color, talla, etc.)'; COMMENT ON TABLE inventory.warehouses IS 'Almacenes físicos'; COMMENT ON TABLE inventory.locations IS 'Ubicaciones dentro de almacenes (estantes, zonas, etc.)'; COMMENT ON TABLE inventory.stock_quants IS 'Cantidades en stock por producto/ubicación/lote'; COMMENT ON TABLE inventory.lots IS 'Lotes de producción y números de serie'; COMMENT ON TABLE inventory.pickings IS 'Albaranes de entrada, salida y transferencia'; COMMENT ON TABLE inventory.stock_moves IS 'Movimientos individuales de inventario'; COMMENT ON TABLE inventory.inventory_adjustments IS 'Ajustes de inventario (conteos físicos)'; COMMENT ON TABLE inventory.inventory_adjustment_lines IS 'Líneas de ajuste de inventario'; -- ===================================================== -- VISTAS ÚTILES -- ===================================================== -- Vista: stock_by_product (Stock por producto) CREATE OR REPLACE VIEW inventory.stock_by_product_view AS SELECT p.id AS product_id, p.code AS product_code, p.name AS product_name, l.id AS location_id, l.complete_name AS location_name, COALESCE(SUM(sq.quantity), 0) AS quantity, COALESCE(SUM(sq.reserved_quantity), 0) AS reserved_quantity, COALESCE(SUM(sq.available_quantity), 0) AS available_quantity FROM inventory.products p CROSS JOIN inventory.locations l LEFT JOIN inventory.stock_quants sq ON sq.product_id = p.id AND sq.location_id = l.id WHERE p.product_type = 'storable' AND l.location_type = 'internal' GROUP BY p.id, p.code, p.name, l.id, l.complete_name; COMMENT ON VIEW inventory.stock_by_product_view IS 'Vista de stock disponible por producto y ubicación'; -- ===================================================== -- COR-025: Stock Routes and Rules -- Equivalente a stock.route y stock.rule de Odoo -- ===================================================== CREATE TYPE inventory.rule_action AS ENUM ('pull', 'push', 'pull_push', 'buy', 'manufacture'); CREATE TYPE inventory.procurement_type AS ENUM ('make_to_stock', 'make_to_order'); -- Tabla: routes (Rutas de abastecimiento) CREATE TABLE inventory.routes ( 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, sequence INTEGER DEFAULT 10, is_active BOOLEAN DEFAULT TRUE, product_selectable BOOLEAN DEFAULT TRUE, product_categ_selectable BOOLEAN DEFAULT TRUE, warehouse_selectable BOOLEAN DEFAULT TRUE, supplied_wh_id UUID REFERENCES inventory.warehouses(id), supplier_wh_id UUID REFERENCES inventory.warehouses(id), company_id UUID REFERENCES core.companies(id), created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); -- Tabla: stock_rules (Reglas de push/pull) CREATE TABLE inventory.stock_rules ( 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, route_id UUID NOT NULL REFERENCES inventory.routes(id) ON DELETE CASCADE, sequence INTEGER DEFAULT 20, action inventory.rule_action NOT NULL, procure_method inventory.procurement_type DEFAULT 'make_to_stock', location_src_id UUID REFERENCES inventory.locations(id), location_dest_id UUID NOT NULL REFERENCES inventory.locations(id), picking_type_id UUID REFERENCES inventory.picking_types(id), delay INTEGER DEFAULT 0, -- Lead time in days partner_address_id UUID REFERENCES core.partners(id), propagate_cancel BOOLEAN DEFAULT FALSE, warehouse_id UUID REFERENCES inventory.warehouses(id), is_active BOOLEAN DEFAULT TRUE, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); -- Tabla: product_routes (Relacion producto-rutas) CREATE TABLE inventory.product_routes ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), product_id UUID NOT NULL REFERENCES inventory.products(id) ON DELETE CASCADE, route_id UUID NOT NULL REFERENCES inventory.routes(id) ON DELETE CASCADE, UNIQUE(product_id, route_id) ); CREATE INDEX idx_routes_tenant ON inventory.routes(tenant_id); CREATE INDEX idx_routes_warehouse ON inventory.routes(supplied_wh_id); CREATE INDEX idx_rules_route ON inventory.stock_rules(route_id); CREATE INDEX idx_rules_locations ON inventory.stock_rules(location_src_id, location_dest_id); CREATE INDEX idx_product_routes_product ON inventory.product_routes(product_id); -- RLS ALTER TABLE inventory.routes ENABLE ROW LEVEL SECURITY; ALTER TABLE inventory.stock_rules ENABLE ROW LEVEL SECURITY; CREATE POLICY tenant_isolation_routes ON inventory.routes USING (tenant_id = get_current_tenant_id()); CREATE POLICY tenant_isolation_stock_rules ON inventory.stock_rules USING (tenant_id = get_current_tenant_id()); COMMENT ON TABLE inventory.routes IS 'COR-025: Stock routes - Equivalent to stock.route'; COMMENT ON TABLE inventory.stock_rules IS 'COR-025: Stock rules - Equivalent to stock.rule'; COMMENT ON TABLE inventory.product_routes IS 'COR-025: Product-route relationship'; -- ===================================================== -- COR-031: Stock Scrap -- Equivalente a stock.scrap de Odoo -- ===================================================== CREATE TYPE inventory.scrap_status AS ENUM ('draft', 'done'); CREATE TABLE inventory.stock_scrap ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, name VARCHAR(100), product_id UUID NOT NULL REFERENCES inventory.products(id), product_uom_id UUID REFERENCES core.uom(id), lot_id UUID REFERENCES inventory.lots(id), scrap_qty DECIMAL(20,6) NOT NULL, scrap_location_id UUID NOT NULL REFERENCES inventory.locations(id), location_id UUID NOT NULL REFERENCES inventory.locations(id), move_id UUID REFERENCES inventory.stock_moves(id), picking_id UUID REFERENCES inventory.pickings(id), origin VARCHAR(255), date_done TIMESTAMP, status inventory.scrap_status DEFAULT 'draft', created_by UUID REFERENCES auth.users(id), created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); CREATE INDEX idx_stock_scrap_tenant ON inventory.stock_scrap(tenant_id); CREATE INDEX idx_stock_scrap_product ON inventory.stock_scrap(product_id); CREATE INDEX idx_stock_scrap_status ON inventory.stock_scrap(status); -- RLS ALTER TABLE inventory.stock_scrap ENABLE ROW LEVEL SECURITY; CREATE POLICY tenant_isolation_stock_scrap ON inventory.stock_scrap USING (tenant_id = get_current_tenant_id()); COMMENT ON TABLE inventory.stock_scrap IS 'COR-031: Stock scrap - Equivalent to stock.scrap'; -- Funcion: validate_scrap CREATE OR REPLACE FUNCTION inventory.validate_scrap(p_scrap_id UUID) RETURNS UUID AS $$ DECLARE v_scrap RECORD; v_move_id UUID; BEGIN SELECT * INTO v_scrap FROM inventory.stock_scrap WHERE id = p_scrap_id; IF NOT FOUND THEN RAISE EXCEPTION 'Scrap record % not found', p_scrap_id; END IF; IF v_scrap.status = 'done' THEN RETURN v_scrap.move_id; END IF; -- Create stock move INSERT INTO inventory.stock_moves ( tenant_id, product_id, product_uom_id, quantity, location_id, location_dest_id, origin, status ) VALUES ( v_scrap.tenant_id, v_scrap.product_id, v_scrap.product_uom_id, v_scrap.scrap_qty, v_scrap.location_id, v_scrap.scrap_location_id, v_scrap.name, 'done' ) RETURNING id INTO v_move_id; -- Update scrap record UPDATE inventory.stock_scrap SET status = 'done', move_id = v_move_id, date_done = NOW(), updated_at = NOW() WHERE id = p_scrap_id; RETURN v_move_id; END; $$ LANGUAGE plpgsql; COMMENT ON FUNCTION inventory.validate_scrap IS 'COR-031: Validate scrap and create stock move'; -- ===================================================== -- COR-040: Stock Quant Packages (Paquetes/Bultos) -- Equivalente a stock.quant.package de Odoo -- ===================================================== CREATE TABLE inventory.packages ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, name VARCHAR(100) NOT NULL, package_type_id UUID, shipping_weight DECIMAL(16,4), pack_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP, location_id UUID REFERENCES inventory.locations(id), company_id UUID REFERENCES core.companies(id), created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE inventory.package_types ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, name VARCHAR(100) NOT NULL, sequence INTEGER DEFAULT 1, barcode VARCHAR(100), height DECIMAL(16,4), width DECIMAL(16,4), packaging_length DECIMAL(16,4), base_weight DECIMAL(16,4), max_weight DECIMAL(16,4), shipper_package_code VARCHAR(50), company_id UUID REFERENCES core.companies(id), created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ); -- Agregar FK a packages ALTER TABLE inventory.packages ADD CONSTRAINT fk_packages_type FOREIGN KEY (package_type_id) REFERENCES inventory.package_types(id); -- Agregar package_id a stock_quants ALTER TABLE inventory.stock_quants ADD COLUMN IF NOT EXISTS package_id UUID REFERENCES inventory.packages(id); CREATE INDEX idx_packages_tenant ON inventory.packages(tenant_id); CREATE INDEX idx_packages_location ON inventory.packages(location_id); CREATE INDEX idx_package_types_tenant ON inventory.package_types(tenant_id); CREATE INDEX idx_stock_quants_package ON inventory.stock_quants(package_id); ALTER TABLE inventory.packages ENABLE ROW LEVEL SECURITY; ALTER TABLE inventory.package_types ENABLE ROW LEVEL SECURITY; CREATE POLICY tenant_isolation_packages ON inventory.packages USING (tenant_id = get_current_tenant_id()); CREATE POLICY tenant_isolation_package_types ON inventory.package_types USING (tenant_id = get_current_tenant_id()); COMMENT ON TABLE inventory.packages IS 'COR-040: Stock packages - Equivalent to stock.quant.package'; COMMENT ON TABLE inventory.package_types IS 'COR-040: Package types - Equivalent to product.packaging'; -- ===================================================== -- COR-041: Putaway Rules (Reglas de ubicacion) -- Equivalente a stock.putaway.rule de Odoo -- ===================================================== CREATE TABLE inventory.putaway_rules ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, product_id UUID REFERENCES inventory.products(id), category_id UUID REFERENCES inventory.product_categories(id), location_in_id UUID NOT NULL REFERENCES inventory.locations(id), location_out_id UUID NOT NULL REFERENCES inventory.locations(id), sequence INTEGER DEFAULT 10, storage_category_id UUID, company_id UUID REFERENCES core.companies(id), is_active BOOLEAN DEFAULT TRUE, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, CONSTRAINT chk_product_or_category CHECK (product_id IS NOT NULL OR category_id IS NOT NULL) ); CREATE INDEX idx_putaway_rules_tenant ON inventory.putaway_rules(tenant_id); CREATE INDEX idx_putaway_rules_product ON inventory.putaway_rules(product_id); CREATE INDEX idx_putaway_rules_category ON inventory.putaway_rules(category_id); CREATE INDEX idx_putaway_rules_location_in ON inventory.putaway_rules(location_in_id); ALTER TABLE inventory.putaway_rules ENABLE ROW LEVEL SECURITY; CREATE POLICY tenant_isolation_putaway_rules ON inventory.putaway_rules USING (tenant_id = get_current_tenant_id()); COMMENT ON TABLE inventory.putaway_rules IS 'COR-041: Putaway rules - Equivalent to stock.putaway.rule'; -- ===================================================== -- COR-042: Storage Categories -- Equivalente a stock.storage.category de Odoo -- ===================================================== CREATE TABLE inventory.storage_categories ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, name VARCHAR(100) NOT NULL, max_weight DECIMAL(16,4), allow_new_product VARCHAR(20) DEFAULT 'mixed', -- mixed, same, empty company_id UUID REFERENCES core.companies(id), created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ); -- Agregar FK a putaway_rules ALTER TABLE inventory.putaway_rules ADD CONSTRAINT fk_putaway_storage FOREIGN KEY (storage_category_id) REFERENCES inventory.storage_categories(id); -- Agregar storage_category_id a locations ALTER TABLE inventory.locations ADD COLUMN IF NOT EXISTS storage_category_id UUID REFERENCES inventory.storage_categories(id); CREATE INDEX idx_storage_categories_tenant ON inventory.storage_categories(tenant_id); ALTER TABLE inventory.storage_categories ENABLE ROW LEVEL SECURITY; CREATE POLICY tenant_isolation_storage_categories ON inventory.storage_categories USING (tenant_id = get_current_tenant_id()); COMMENT ON TABLE inventory.storage_categories IS 'COR-042: Storage categories - Equivalent to stock.storage.category'; -- ===================================================== -- COR-043: Campos adicionales en tablas existentes -- ===================================================== -- Tracking en products (lot/serial) ALTER TABLE inventory.products ADD COLUMN IF NOT EXISTS tracking VARCHAR(20) DEFAULT 'none'; -- none, lot, serial ALTER TABLE inventory.products ADD COLUMN IF NOT EXISTS sale_ok BOOLEAN DEFAULT TRUE; ALTER TABLE inventory.products ADD COLUMN IF NOT EXISTS purchase_ok BOOLEAN DEFAULT TRUE; ALTER TABLE inventory.products ADD COLUMN IF NOT EXISTS invoice_policy VARCHAR(20) DEFAULT 'order'; -- order, delivery ALTER TABLE inventory.products ADD COLUMN IF NOT EXISTS expense_policy VARCHAR(20); -- no, cost, sales_price ALTER TABLE inventory.products ADD COLUMN IF NOT EXISTS service_type VARCHAR(20); -- manual, timesheet ALTER TABLE inventory.products ADD COLUMN IF NOT EXISTS sale_delay INTEGER DEFAULT 0; ALTER TABLE inventory.products ADD COLUMN IF NOT EXISTS purchase_method VARCHAR(20) DEFAULT 'receive'; -- purchase, receive ALTER TABLE inventory.products ADD COLUMN IF NOT EXISTS produce_delay INTEGER DEFAULT 1; -- Campos en stock_quants ALTER TABLE inventory.stock_quants ADD COLUMN IF NOT EXISTS reserved_quantity DECIMAL(20,6) DEFAULT 0; ALTER TABLE inventory.stock_quants ADD COLUMN IF NOT EXISTS inventory_quantity DECIMAL(20,6); ALTER TABLE inventory.stock_quants ADD COLUMN IF NOT EXISTS inventory_diff_quantity DECIMAL(20,6); ALTER TABLE inventory.stock_quants ADD COLUMN IF NOT EXISTS inventory_date DATE; ALTER TABLE inventory.stock_quants ADD COLUMN IF NOT EXISTS user_id UUID REFERENCES auth.users(id); -- Campos en pickings ALTER TABLE inventory.pickings ADD COLUMN IF NOT EXISTS show_check_availability BOOLEAN DEFAULT TRUE; ALTER TABLE inventory.pickings ADD COLUMN IF NOT EXISTS show_validate BOOLEAN DEFAULT TRUE; ALTER TABLE inventory.pickings ADD COLUMN IF NOT EXISTS show_allocation BOOLEAN DEFAULT FALSE; ALTER TABLE inventory.pickings ADD COLUMN IF NOT EXISTS immediate_transfer BOOLEAN DEFAULT FALSE; ALTER TABLE inventory.pickings ADD COLUMN IF NOT EXISTS printed BOOLEAN DEFAULT FALSE; ALTER TABLE inventory.pickings ADD COLUMN IF NOT EXISTS is_locked BOOLEAN DEFAULT TRUE; ALTER TABLE inventory.pickings ADD COLUMN IF NOT EXISTS package_ids UUID[]; ALTER TABLE inventory.pickings ADD COLUMN IF NOT EXISTS carrier_id UUID; ALTER TABLE inventory.pickings ADD COLUMN IF NOT EXISTS carrier_tracking_ref VARCHAR(255); ALTER TABLE inventory.pickings ADD COLUMN IF NOT EXISTS weight DECIMAL(16,4); ALTER TABLE inventory.pickings ADD COLUMN IF NOT EXISTS shipping_weight DECIMAL(16,4); -- Campos en stock_moves ALTER TABLE inventory.stock_moves ADD COLUMN IF NOT EXISTS procure_method VARCHAR(20) DEFAULT 'make_to_stock'; ALTER TABLE inventory.stock_moves ADD COLUMN IF NOT EXISTS rule_id UUID REFERENCES inventory.stock_rules(id); ALTER TABLE inventory.stock_moves ADD COLUMN IF NOT EXISTS propagate_cancel BOOLEAN DEFAULT FALSE; ALTER TABLE inventory.stock_moves ADD COLUMN IF NOT EXISTS delay_alert_date DATE; ALTER TABLE inventory.stock_moves ADD COLUMN IF NOT EXISTS scrapped BOOLEAN DEFAULT FALSE; ALTER TABLE inventory.stock_moves ADD COLUMN IF NOT EXISTS is_inventory BOOLEAN DEFAULT FALSE; ALTER TABLE inventory.stock_moves ADD COLUMN IF NOT EXISTS priority VARCHAR(10) DEFAULT '0'; -- 0=normal, 1=urgent -- Campos en warehouses ALTER TABLE inventory.warehouses ADD COLUMN IF NOT EXISTS buy_to_resupply BOOLEAN DEFAULT TRUE; ALTER TABLE inventory.warehouses ADD COLUMN IF NOT EXISTS manufacture_to_resupply BOOLEAN DEFAULT FALSE; ALTER TABLE inventory.warehouses ADD COLUMN IF NOT EXISTS reception_steps VARCHAR(20) DEFAULT 'one_step'; -- one_step, two_steps, three_steps ALTER TABLE inventory.warehouses ADD COLUMN IF NOT EXISTS delivery_steps VARCHAR(20) DEFAULT 'ship_only'; -- ship_only, pick_ship, pick_pack_ship ALTER TABLE inventory.warehouses ADD COLUMN IF NOT EXISTS wh_input_stock_loc_id UUID REFERENCES inventory.locations(id); ALTER TABLE inventory.warehouses ADD COLUMN IF NOT EXISTS wh_qc_stock_loc_id UUID REFERENCES inventory.locations(id); ALTER TABLE inventory.warehouses ADD COLUMN IF NOT EXISTS wh_output_stock_loc_id UUID REFERENCES inventory.locations(id); ALTER TABLE inventory.warehouses ADD COLUMN IF NOT EXISTS wh_pack_stock_loc_id UUID REFERENCES inventory.locations(id); ALTER TABLE inventory.warehouses ADD COLUMN IF NOT EXISTS pick_type_id UUID REFERENCES inventory.picking_types(id); ALTER TABLE inventory.warehouses ADD COLUMN IF NOT EXISTS pack_type_id UUID REFERENCES inventory.picking_types(id); -- Campos en locations ALTER TABLE inventory.locations ADD COLUMN IF NOT EXISTS removal_strategy_id UUID; ALTER TABLE inventory.locations ADD COLUMN IF NOT EXISTS putaway_rule_ids UUID[]; ALTER TABLE inventory.locations ADD COLUMN IF NOT EXISTS cyclic_inventory_frequency INTEGER DEFAULT 0; ALTER TABLE inventory.locations ADD COLUMN IF NOT EXISTS last_inventory_date DATE; ALTER TABLE inventory.locations ADD COLUMN IF NOT EXISTS next_inventory_date DATE; -- Campos en lots ALTER TABLE inventory.lots ADD COLUMN IF NOT EXISTS use_date DATE; ALTER TABLE inventory.lots ADD COLUMN IF NOT EXISTS removal_date DATE; ALTER TABLE inventory.lots ADD COLUMN IF NOT EXISTS alert_date DATE; ALTER TABLE inventory.lots ADD COLUMN IF NOT EXISTS product_qty DECIMAL(20,6); COMMENT ON COLUMN inventory.products.tracking IS 'COR-043: Product tracking mode (none/lot/serial)'; COMMENT ON COLUMN inventory.stock_quants.reserved_quantity IS 'COR-043: Reserved quantity for orders'; -- ===================================================== -- COR-044: Removal Strategies -- Equivalente a product.removal de Odoo -- ===================================================== CREATE TABLE inventory.removal_strategies ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), name VARCHAR(100) NOT NULL, method VARCHAR(20) NOT NULL, -- fifo, lifo, closest, least_packages created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ); -- Seed data INSERT INTO inventory.removal_strategies (name, method) VALUES ('First In First Out (FIFO)', 'fifo'), ('Last In First Out (LIFO)', 'lifo'), ('Closest Location', 'closest'), ('Least Packages', 'least_packages'); -- FK en locations ALTER TABLE inventory.locations ADD CONSTRAINT fk_locations_removal FOREIGN KEY (removal_strategy_id) REFERENCES inventory.removal_strategies(id); COMMENT ON TABLE inventory.removal_strategies IS 'COR-044: Removal strategies - Equivalent to product.removal'; -- ===================================================== -- FIN DEL SCHEMA INVENTORY -- =====================================================