🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1329 lines
51 KiB
PL/PgSQL
1329 lines
51 KiB
PL/PgSQL
-- =====================================================
|
|
-- 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
|
|
-- =====================================================
|