Sistema NEXUS v3.4 migrado con: Estructura principal: - core/orchestration: Sistema SIMCO + CAPVED (27 directivas, 28 perfiles) - core/catalog: Catalogo de funcionalidades reutilizables - shared/knowledge-base: Base de conocimiento compartida - devtools/scripts: Herramientas de desarrollo - control-plane/registries: Control de servicios y CI/CD - orchestration/: Configuracion de orquestacion de agentes Proyectos incluidos (11): - gamilit (submodule -> GitHub) - trading-platform (OrbiquanTIA) - erp-suite con 5 verticales: - erp-core, construccion, vidrio-templado - mecanicas-diesel, retail, clinicas - betting-analytics - inmobiliaria-analytics - platform_marketing_content - pos-micro, erp-basico Configuracion: - .gitignore completo para Node.js/Python/Docker - gamilit como submodule (git@github.com:rckrdmrd/gamilit-workspace.git) - Sistema de puertos estandarizado (3005-3199) Generated with NEXUS v3.4 Migration System EPIC-010: Configuracion Git y Repositorios
66 KiB
66 KiB
DDL-SPEC: Schemas purchasing_management & inventory_management
Identificacion
| Campo | Valor |
|---|---|
| Schemas | purchasing_management, inventory_management |
| Modulo | MAI-004 - Compras e Inventarios |
| Vertical | Construccion |
| Version | 1.0 |
| Estado | En Diseno |
| Autor | Database-Agent |
| Fecha | 2025-12-06 |
Descripcion General
Los schemas purchasing_management e inventory_management gestionan el ciclo completo de compras e inventarios para proyectos de construccion: desde requisiciones de materiales, cotizaciones, ordenes de compra, hasta el control de almacenes, movimientos de inventario y valoracion PEPS (Primeras Entradas, Primeras Salidas).
Alcance
- Gestion de requisiciones de materiales con flujo de aprobacion multi-nivel
- Solicitudes de cotizacion (RFQ) y recepcion de cotizaciones
- Generacion y seguimiento de ordenes de compra
- Control de almacenes (generales, por proyecto, temporales)
- Movimientos de inventario (entradas, salidas, traspasos, ajustes)
- Valoracion de inventario mediante metodo PEPS
- Niveles de stock en tiempo real con reservas
- Inventarios fisicos y ajustes
RF Cubiertos
| RF | Titulo | Tablas |
|---|---|---|
| RF-COMP-001 | Requisiciones de Materiales | requisitions |
| RF-COMP-002 | Cotizaciones y RFQs | quotations, rfqs |
| RF-COMP-003 | Ordenes de Compra | purchase_orders, po_receipts |
| RF-INV-001 | Gestion de Almacenes | warehouses, warehouse_locations |
| RF-INV-002 | Movimientos de Inventario | inventory_movements |
| RF-INV-003 | Niveles de Stock | stock_levels, inventory_lots |
Diagrama Entidad-Relacion
erDiagram
%% Purchasing Management
requisitions ||--o{ purchase_orders : "genera"
requisitions ||--o{ rfqs : "origina"
rfqs ||--o{ quotations : "recibe"
quotations ||--o| purchase_orders : "convierte_en"
purchase_orders ||--o{ po_receipts : "recibe_material"
%% Inventory Management
warehouses ||--o{ warehouse_locations : "tiene_ubicaciones"
warehouses ||--o{ inventory_movements : "registra_movimientos"
warehouses ||--o{ stock_levels : "mantiene_stock"
warehouses ||--o{ inventory_lots : "almacena_lotes"
inventory_movements ||--o{ inventory_lots : "consume_lotes"
po_receipts }o--|| warehouses : "destino"
stock_levels ||--o{ inventory_lots : "compuesto_por"
requisitions {
uuid id PK
uuid tenant_id FK
varchar code UK
uuid project_id FK
uuid requested_by FK
date required_date
requisition_status status
jsonb items
decimal estimated_total
jsonb approval_flow
text rejected_reason
timestamptz created_at
timestamptz updated_at
uuid created_by FK
uuid updated_by FK
boolean is_active
timestamptz deleted_at
uuid deleted_by FK
}
rfqs {
uuid id PK
uuid tenant_id FK
varchar code UK
uuid requisition_id FK
uuid project_id FK
date issue_date
date closing_date
rfq_status status
jsonb items
jsonb invited_suppliers
text terms_conditions
timestamptz created_at
timestamptz updated_at
uuid created_by FK
uuid updated_by FK
boolean is_active
}
quotations {
uuid id PK
uuid tenant_id FK
varchar code UK
uuid rfq_id FK
uuid supplier_id FK
date quote_date
date valid_until
quotation_status status
jsonb items
decimal subtotal
decimal tax
decimal total
int delivery_days
varchar payment_terms
decimal payment_discount
text notes
timestamptz created_at
timestamptz updated_at
uuid created_by FK
uuid updated_by FK
boolean is_active
}
purchase_orders {
uuid id PK
uuid tenant_id FK
varchar code UK
uuid supplier_id FK
uuid project_id FK
uuid requisition_id FK
uuid rfq_id FK
uuid quotation_id FK
date order_date
date delivery_date
text delivery_address
jsonb items
decimal subtotal
decimal tax
decimal total
po_status status
varchar payment_terms
int payment_terms_days
decimal early_payment_discount
boolean requires_advance
decimal advance_percentage
int warranty_days
text special_conditions
uuid approved_by FK
timestamptz approved_at
timestamptz sent_at
timestamptz created_at
timestamptz updated_at
uuid created_by FK
uuid updated_by FK
boolean is_active
}
po_receipts {
uuid id PK
uuid tenant_id FK
varchar code UK
uuid purchase_order_id FK
uuid warehouse_id FK
date receipt_date
uuid received_by FK
jsonb items
varchar delivery_note
varchar transport_company
text notes
text[] attachments
timestamptz created_at
uuid created_by FK
}
warehouses {
uuid id PK
uuid tenant_id FK
varchar code UK
varchar name
warehouse_type type
uuid project_id FK
text address
varchar city
varchar state
uuid managed_by FK
decimal total_area
decimal covered_area
jsonb settings
timestamptz created_at
timestamptz updated_at
uuid created_by FK
uuid updated_by FK
boolean is_active
timestamptz deleted_at
uuid deleted_by FK
}
warehouse_locations {
uuid id PK
uuid tenant_id FK
uuid warehouse_id FK
varchar zone
varchar position
varchar code UK
varchar description
decimal capacity_m3
timestamptz created_at
boolean is_active
}
inventory_movements {
uuid id PK
uuid tenant_id FK
varchar code UK
uuid warehouse_id FK
movement_type type
date movement_date
movement_source source_type
uuid source_id
uuid project_id FK
uuid budget_item_id FK
uuid transfer_warehouse_id FK
jsonb items
decimal total_value
text notes
uuid authorized_by FK
uuid recorded_by FK
transfer_status transfer_status
timestamptz received_at
timestamptz created_at
uuid created_by FK
}
stock_levels {
uuid id PK
uuid tenant_id FK
uuid warehouse_id FK
uuid material_id FK
uuid location_id FK
decimal quantity
decimal reserved_quantity
decimal available_quantity
decimal average_cost
decimal total_value
date last_movement_date
date last_entry_date
date last_exit_date
timestamptz updated_at
}
inventory_lots {
uuid id PK
uuid tenant_id FK
uuid warehouse_id FK
uuid material_id FK
varchar lot_number
date entry_date
decimal quantity
decimal remaining_quantity
decimal unit_cost
movement_source source_type
uuid source_id
boolean is_depleted
timestamptz created_at
}
Schema: purchasing_management
Extensiones Necesarias
-- Extensiones ya creadas en core
-- uuid-ossp, pg_trgm, btree_gist, pgcrypto
1. ENUMs
-- ============================================================================
-- ENUMs - Purchasing Management
-- ============================================================================
-- Estado de requisiciones
CREATE TYPE purchasing_management.requisition_status AS ENUM (
'draft', -- Borrador
'pending', -- Pendiente de aprobacion
'approved', -- Aprobada
'rejected', -- Rechazada
'quoted', -- En proceso de cotizacion
'ordered', -- Convertida a OC
'cancelled' -- Cancelada
);
COMMENT ON TYPE purchasing_management.requisition_status IS 'Estados del ciclo de vida de una requisicion';
-- Estado de RFQs
CREATE TYPE purchasing_management.rfq_status AS ENUM (
'draft', -- Borrador
'sent', -- Enviada a proveedores
'receiving', -- Recibiendo cotizaciones
'evaluating', -- En evaluacion
'awarded', -- Adjudicada
'closed', -- Cerrada
'cancelled' -- Cancelada
);
COMMENT ON TYPE purchasing_management.rfq_status IS 'Estados de solicitud de cotizacion';
-- Estado de cotizaciones
CREATE TYPE purchasing_management.quotation_status AS ENUM (
'received', -- Recibida
'under_review', -- En revision
'accepted', -- Aceptada
'rejected', -- Rechazada
'expired' -- Vencida
);
COMMENT ON TYPE purchasing_management.quotation_status IS 'Estados de cotizacion de proveedor';
-- Estado de ordenes de compra
CREATE TYPE purchasing_management.po_status AS ENUM (
'draft', -- Borrador
'pending', -- Pendiente de aprobacion
'approved', -- Aprobada
'sent', -- Enviada a proveedor
'partially_received', -- Parcialmente recibida
'received', -- Recibida completamente
'cancelled', -- Cancelada
'closed' -- Cerrada
);
COMMENT ON TYPE purchasing_management.po_status IS 'Estados del ciclo de vida de orden de compra';
2. Tabla: requisitions
-- ============================================================================
-- Schema: purchasing_management
-- Tabla: requisitions
-- Descripcion: Requisiciones de materiales para proyectos
-- Modulo: MAI-004
-- ============================================================================
CREATE TABLE IF NOT EXISTS purchasing_management.requisitions (
-- Primary Key
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
-- Multi-tenant
tenant_id UUID NOT NULL REFERENCES core_tenants.tenants(id) ON DELETE CASCADE,
-- Identificacion
code VARCHAR(30) NOT NULL,
-- Relaciones
project_id UUID NOT NULL, -- FK a projects schema
requested_by UUID NOT NULL REFERENCES core_users.users(id),
-- Datos principales
required_date DATE NOT NULL,
urgency VARCHAR(20) NOT NULL DEFAULT 'normal',
-- Items (JSONB estructura flexible)
items JSONB NOT NULL,
/* Estructura:
[{
materialId: UUID,
materialCode: string,
description: string,
quantity: number,
unit: string,
budgetedPrice: number,
budgetItemId: UUID,
notes: string
}]
*/
justification TEXT,
estimated_total DECIMAL(15,2) NOT NULL,
-- Estado y flujo de aprobacion
status purchasing_management.requisition_status NOT NULL DEFAULT 'draft',
approval_flow JSONB,
/* Estructura:
[{
level: number,
approverRole: string,
approverId: UUID,
approverName: string,
status: 'pending'|'approved'|'rejected',
comments: string,
approvedAt: timestamp
}]
*/
rejected_reason TEXT,
-- Auditoria
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
created_by UUID REFERENCES core_users.users(id),
updated_by UUID REFERENCES core_users.users(id),
-- Soft delete
is_active BOOLEAN NOT NULL DEFAULT true,
deleted_at TIMESTAMPTZ,
deleted_by UUID REFERENCES core_users.users(id),
-- Constraints
CONSTRAINT chk_requisitions_urgency CHECK (urgency IN ('normal', 'urgent', 'emergency')),
CONSTRAINT chk_requisitions_estimated_total CHECK (estimated_total >= 0),
CONSTRAINT uq_requisitions_code_tenant UNIQUE (tenant_id, code)
);
-- Comentarios
COMMENT ON TABLE purchasing_management.requisitions IS 'Requisiciones de materiales para proyectos de construccion';
COMMENT ON COLUMN purchasing_management.requisitions.tenant_id IS 'ID del tenant (constructora)';
COMMENT ON COLUMN purchasing_management.requisitions.code IS 'Codigo unico de requisicion (ej: REQ-2025-00001)';
COMMENT ON COLUMN purchasing_management.requisitions.items IS 'Items solicitados en formato JSONB';
COMMENT ON COLUMN purchasing_management.requisitions.approval_flow IS 'Flujo de aprobacion multi-nivel segun monto';
-- Indices
CREATE INDEX idx_requisitions_tenant_id ON purchasing_management.requisitions(tenant_id);
CREATE INDEX idx_requisitions_project_id ON purchasing_management.requisitions(project_id);
CREATE INDEX idx_requisitions_status ON purchasing_management.requisitions(status) WHERE is_active = true;
CREATE INDEX idx_requisitions_requested_by ON purchasing_management.requisitions(requested_by);
CREATE INDEX idx_requisitions_created_at ON purchasing_management.requisitions(created_at DESC);
CREATE INDEX idx_requisitions_required_date ON purchasing_management.requisitions(required_date);
-- Indice GIN para busqueda en JSONB
CREATE INDEX idx_requisitions_items_gin ON purchasing_management.requisitions USING gin (items jsonb_path_ops);
-- RLS Policies
ALTER TABLE purchasing_management.requisitions ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation_select ON purchasing_management.requisitions
FOR SELECT
USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid);
CREATE POLICY tenant_isolation_insert ON purchasing_management.requisitions
FOR INSERT
WITH CHECK (tenant_id = current_setting('app.current_tenant_id', true)::uuid);
CREATE POLICY tenant_isolation_update ON purchasing_management.requisitions
FOR UPDATE
USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid);
CREATE POLICY tenant_isolation_delete ON purchasing_management.requisitions
FOR DELETE
USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid);
3. Tabla: rfqs (Request for Quotation)
-- ============================================================================
-- Schema: purchasing_management
-- Tabla: rfqs
-- Descripcion: Solicitudes de cotizacion a proveedores
-- Modulo: MAI-004
-- ============================================================================
CREATE TABLE IF NOT EXISTS purchasing_management.rfqs (
-- Primary Key
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
-- Multi-tenant
tenant_id UUID NOT NULL REFERENCES core_tenants.tenants(id) ON DELETE CASCADE,
-- Identificacion
code VARCHAR(30) NOT NULL,
-- Relaciones
requisition_id UUID REFERENCES purchasing_management.requisitions(id),
project_id UUID NOT NULL,
-- Datos principales
issue_date DATE NOT NULL DEFAULT CURRENT_DATE,
closing_date DATE NOT NULL,
-- Items a cotizar
items JSONB NOT NULL,
/* Estructura:
[{
materialId: UUID,
materialCode: string,
description: string,
quantity: number,
unit: string,
technicalSpecs: string
}]
*/
-- Proveedores invitados
invited_suppliers JSONB NOT NULL,
/* Estructura:
[{
supplierId: UUID,
supplierName: string,
contactEmail: string,
invitedAt: timestamp,
notifiedAt: timestamp
}]
*/
-- Estado
status purchasing_management.rfq_status NOT NULL DEFAULT 'draft',
-- Terminos y condiciones
terms_conditions TEXT,
delivery_address TEXT,
payment_terms VARCHAR(100),
-- Configuracion
metadata JSONB DEFAULT '{}',
-- Auditoria
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
created_by UUID REFERENCES core_users.users(id),
updated_by UUID REFERENCES core_users.users(id),
-- Soft delete
is_active BOOLEAN NOT NULL DEFAULT true,
deleted_at TIMESTAMPTZ,
deleted_by UUID REFERENCES core_users.users(id),
-- Constraints
CONSTRAINT chk_rfqs_closing_date CHECK (closing_date >= issue_date),
CONSTRAINT uq_rfqs_code_tenant UNIQUE (tenant_id, code)
);
COMMENT ON TABLE purchasing_management.rfqs IS 'Solicitudes de cotizacion (RFQ) a proveedores';
COMMENT ON COLUMN purchasing_management.rfqs.code IS 'Codigo unico RFQ (ej: RFQ-2025-00001)';
-- Indices
CREATE INDEX idx_rfqs_tenant_id ON purchasing_management.rfqs(tenant_id);
CREATE INDEX idx_rfqs_requisition_id ON purchasing_management.rfqs(requisition_id);
CREATE INDEX idx_rfqs_project_id ON purchasing_management.rfqs(project_id);
CREATE INDEX idx_rfqs_status ON purchasing_management.rfqs(status) WHERE is_active = true;
CREATE INDEX idx_rfqs_closing_date ON purchasing_management.rfqs(closing_date);
-- RLS Policies
ALTER TABLE purchasing_management.rfqs ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation_select ON purchasing_management.rfqs
FOR SELECT USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid);
CREATE POLICY tenant_isolation_insert ON purchasing_management.rfqs
FOR INSERT WITH CHECK (tenant_id = current_setting('app.current_tenant_id', true)::uuid);
CREATE POLICY tenant_isolation_update ON purchasing_management.rfqs
FOR UPDATE USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid);
CREATE POLICY tenant_isolation_delete ON purchasing_management.rfqs
FOR DELETE USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid);
4. Tabla: quotations
-- ============================================================================
-- Schema: purchasing_management
-- Tabla: quotations
-- Descripcion: Cotizaciones recibidas de proveedores
-- Modulo: MAI-004
-- ============================================================================
CREATE TABLE IF NOT EXISTS purchasing_management.quotations (
-- Primary Key
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
-- Multi-tenant
tenant_id UUID NOT NULL REFERENCES core_tenants.tenants(id) ON DELETE CASCADE,
-- Identificacion
code VARCHAR(30) NOT NULL,
-- Relaciones
rfq_id UUID NOT NULL REFERENCES purchasing_management.rfqs(id),
supplier_id UUID NOT NULL, -- FK a core_catalogs.contacts
-- Datos de cotizacion
quote_date DATE NOT NULL DEFAULT CURRENT_DATE,
valid_until DATE NOT NULL,
quote_number VARCHAR(50), -- Numero de cotizacion del proveedor
-- Items cotizados
items JSONB NOT NULL,
/* Estructura:
[{
rfqItemId: string,
materialId: UUID,
description: string,
quantity: number,
unit: string,
unitPrice: number,
subtotal: number,
deliveryDays: number,
brand: string,
notes: string
}]
*/
-- Totales
subtotal DECIMAL(15,2) NOT NULL,
tax DECIMAL(15,2) NOT NULL DEFAULT 0,
total DECIMAL(15,2) NOT NULL,
-- Condiciones
delivery_days INT NOT NULL,
payment_terms VARCHAR(100),
payment_discount DECIMAL(5,2) DEFAULT 0,
warranty_days INT DEFAULT 30,
includes_delivery BOOLEAN DEFAULT true,
-- Estado
status purchasing_management.quotation_status NOT NULL DEFAULT 'received',
-- Evaluacion
evaluation_score DECIMAL(5,2), -- Score de evaluacion (0-100)
evaluation_notes TEXT,
evaluated_by UUID REFERENCES core_users.users(id),
evaluated_at TIMESTAMPTZ,
-- Adjuntos
attachments TEXT[],
notes TEXT,
-- Auditoria
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
created_by UUID REFERENCES core_users.users(id),
updated_by UUID REFERENCES core_users.users(id),
-- Soft delete
is_active BOOLEAN NOT NULL DEFAULT true,
deleted_at TIMESTAMPTZ,
deleted_by UUID REFERENCES core_users.users(id),
-- Constraints
CONSTRAINT chk_quotations_totals CHECK (total = subtotal + tax),
CONSTRAINT chk_quotations_valid_until CHECK (valid_until >= quote_date),
CONSTRAINT chk_quotations_evaluation_score CHECK (evaluation_score IS NULL OR (evaluation_score >= 0 AND evaluation_score <= 100)),
CONSTRAINT uq_quotations_code_tenant UNIQUE (tenant_id, code)
);
COMMENT ON TABLE purchasing_management.quotations IS 'Cotizaciones recibidas de proveedores';
COMMENT ON COLUMN purchasing_management.quotations.evaluation_score IS 'Score de evaluacion 0-100 basado en precio, tiempo, calidad';
-- Indices
CREATE INDEX idx_quotations_tenant_id ON purchasing_management.quotations(tenant_id);
CREATE INDEX idx_quotations_rfq_id ON purchasing_management.quotations(rfq_id);
CREATE INDEX idx_quotations_supplier_id ON purchasing_management.quotations(supplier_id);
CREATE INDEX idx_quotations_status ON purchasing_management.quotations(status) WHERE is_active = true;
CREATE INDEX idx_quotations_valid_until ON purchasing_management.quotations(valid_until);
-- RLS Policies
ALTER TABLE purchasing_management.quotations ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation_select ON purchasing_management.quotations
FOR SELECT USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid);
CREATE POLICY tenant_isolation_insert ON purchasing_management.quotations
FOR INSERT WITH CHECK (tenant_id = current_setting('app.current_tenant_id', true)::uuid);
CREATE POLICY tenant_isolation_update ON purchasing_management.quotations
FOR UPDATE USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid);
CREATE POLICY tenant_isolation_delete ON purchasing_management.quotations
FOR DELETE USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid);
5. Tabla: purchase_orders
-- ============================================================================
-- Schema: purchasing_management
-- Tabla: purchase_orders
-- Descripcion: Ordenes de compra a proveedores
-- Modulo: MAI-004
-- ============================================================================
CREATE TABLE IF NOT EXISTS purchasing_management.purchase_orders (
-- Primary Key
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
-- Multi-tenant
tenant_id UUID NOT NULL REFERENCES core_tenants.tenants(id) ON DELETE CASCADE,
-- Identificacion
code VARCHAR(30) NOT NULL,
-- Relaciones
supplier_id UUID NOT NULL, -- FK a core_catalogs.contacts
project_id UUID NOT NULL,
requisition_id UUID REFERENCES purchasing_management.requisitions(id),
rfq_id UUID REFERENCES purchasing_management.rfqs(id),
quotation_id UUID REFERENCES purchasing_management.quotations(id),
-- Fechas
order_date DATE NOT NULL DEFAULT CURRENT_DATE,
delivery_date DATE NOT NULL,
-- Entrega
delivery_address TEXT NOT NULL,
delivery_contact VARCHAR(100),
delivery_phone VARCHAR(20),
-- Items
items JSONB NOT NULL,
/* Estructura:
[{
materialId: UUID,
materialCode: string,
description: string,
quantity: number,
unit: string,
unitPrice: number,
subtotal: number,
budgetItemId: UUID,
notes: string
}]
*/
-- Totales
subtotal DECIMAL(15,2) NOT NULL,
tax DECIMAL(15,2) NOT NULL DEFAULT 0,
total DECIMAL(15,2) NOT NULL,
-- Estado
status purchasing_management.po_status NOT NULL DEFAULT 'draft',
-- Condiciones de pago
payment_terms VARCHAR(100),
payment_terms_days INT DEFAULT 30,
early_payment_discount DECIMAL(5,2) DEFAULT 0,
requires_advance BOOLEAN DEFAULT false,
advance_percentage DECIMAL(5,2),
-- Otras condiciones
warranty_days INT DEFAULT 30,
includes_delivery BOOLEAN DEFAULT true,
special_conditions TEXT,
-- Aprobacion
approved_by UUID REFERENCES core_users.users(id),
approved_at TIMESTAMPTZ,
-- Envio
sent_at TIMESTAMPTZ,
sent_by UUID REFERENCES core_users.users(id),
-- Adjuntos
attachments TEXT[],
-- Auditoria
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
created_by UUID REFERENCES core_users.users(id),
updated_by UUID REFERENCES core_users.users(id),
-- Soft delete
is_active BOOLEAN NOT NULL DEFAULT true,
deleted_at TIMESTAMPTZ,
deleted_by UUID REFERENCES core_users.users(id),
-- Constraints
CONSTRAINT chk_purchase_orders_totals CHECK (total = subtotal + tax),
CONSTRAINT chk_purchase_orders_delivery_date CHECK (delivery_date >= order_date),
CONSTRAINT chk_purchase_orders_advance CHECK (
(requires_advance = false AND advance_percentage IS NULL) OR
(requires_advance = true AND advance_percentage > 0 AND advance_percentage <= 100)
),
CONSTRAINT uq_purchase_orders_code_tenant UNIQUE (tenant_id, code)
);
COMMENT ON TABLE purchasing_management.purchase_orders IS 'Ordenes de compra a proveedores';
COMMENT ON COLUMN purchasing_management.purchase_orders.code IS 'Codigo unico OC (ej: OC-2025-00001)';
-- Indices
CREATE INDEX idx_purchase_orders_tenant_id ON purchasing_management.purchase_orders(tenant_id);
CREATE INDEX idx_purchase_orders_supplier_id ON purchasing_management.purchase_orders(supplier_id);
CREATE INDEX idx_purchase_orders_project_id ON purchasing_management.purchase_orders(project_id);
CREATE INDEX idx_purchase_orders_status ON purchasing_management.purchase_orders(status) WHERE is_active = true;
CREATE INDEX idx_purchase_orders_delivery_date ON purchasing_management.purchase_orders(delivery_date);
CREATE INDEX idx_purchase_orders_order_date ON purchasing_management.purchase_orders(order_date DESC);
-- RLS Policies
ALTER TABLE purchasing_management.purchase_orders ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation_select ON purchasing_management.purchase_orders
FOR SELECT USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid);
CREATE POLICY tenant_isolation_insert ON purchasing_management.purchase_orders
FOR INSERT WITH CHECK (tenant_id = current_setting('app.current_tenant_id', true)::uuid);
CREATE POLICY tenant_isolation_update ON purchasing_management.purchase_orders
FOR UPDATE USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid);
CREATE POLICY tenant_isolation_delete ON purchasing_management.purchase_orders
FOR DELETE USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid);
6. Tabla: po_receipts
-- ============================================================================
-- Schema: purchasing_management
-- Tabla: po_receipts
-- Descripcion: Recepciones de material de ordenes de compra
-- Modulo: MAI-004
-- ============================================================================
CREATE TABLE IF NOT EXISTS purchasing_management.po_receipts (
-- Primary Key
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
-- Multi-tenant
tenant_id UUID NOT NULL REFERENCES core_tenants.tenants(id) ON DELETE CASCADE,
-- Identificacion
code VARCHAR(30) NOT NULL,
-- Relaciones
purchase_order_id UUID NOT NULL REFERENCES purchasing_management.purchase_orders(id),
warehouse_id UUID NOT NULL, -- FK a inventory_management.warehouses
-- Datos de recepcion
receipt_date DATE NOT NULL DEFAULT CURRENT_DATE,
received_by UUID NOT NULL REFERENCES core_users.users(id),
-- Items recibidos
items JSONB NOT NULL,
/* Estructura:
[{
poItemId: string,
materialId: UUID,
materialCode: string,
description: string,
orderedQuantity: number,
receivedQuantity: number,
acceptedQuantity: number,
rejectedQuantity: number,
unit: string,
unitCost: number,
rejectionReason: string
}]
*/
-- Informacion de entrega
delivery_note VARCHAR(50),
transport_company VARCHAR(100),
driver_name VARCHAR(100),
vehicle_plates VARCHAR(20),
notes TEXT,
attachments TEXT[],
-- Auditoria
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
created_by UUID REFERENCES core_users.users(id),
-- Constraints
CONSTRAINT uq_po_receipts_code_tenant UNIQUE (tenant_id, code)
);
COMMENT ON TABLE purchasing_management.po_receipts IS 'Recepciones de material de ordenes de compra';
COMMENT ON COLUMN purchasing_management.po_receipts.code IS 'Codigo unico recepcion (ej: REC-2025-00001)';
-- Indices
CREATE INDEX idx_po_receipts_tenant_id ON purchasing_management.po_receipts(tenant_id);
CREATE INDEX idx_po_receipts_purchase_order_id ON purchasing_management.po_receipts(purchase_order_id);
CREATE INDEX idx_po_receipts_warehouse_id ON purchasing_management.po_receipts(warehouse_id);
CREATE INDEX idx_po_receipts_receipt_date ON purchasing_management.po_receipts(receipt_date DESC);
-- RLS Policies
ALTER TABLE purchasing_management.po_receipts ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation_select ON purchasing_management.po_receipts
FOR SELECT USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid);
CREATE POLICY tenant_isolation_insert ON purchasing_management.po_receipts
FOR INSERT WITH CHECK (tenant_id = current_setting('app.current_tenant_id', true)::uuid);
CREATE POLICY tenant_isolation_update ON purchasing_management.po_receipts
FOR UPDATE USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid);
CREATE POLICY tenant_isolation_delete ON purchasing_management.po_receipts
FOR DELETE USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid);
Schema: inventory_management
1. ENUMs
-- ============================================================================
-- ENUMs - Inventory Management
-- ============================================================================
-- Tipo de almacen
CREATE TYPE inventory_management.warehouse_type AS ENUM (
'general', -- Almacen general corporativo
'project', -- Almacen de proyecto especifico
'temporary' -- Almacen temporal
);
COMMENT ON TYPE inventory_management.warehouse_type IS 'Tipos de almacen';
-- Tipo de movimiento
CREATE TYPE inventory_management.movement_type AS ENUM (
'entry', -- Entrada de material
'exit', -- Salida de material
'transfer_out', -- Traspaso salida
'transfer_in', -- Traspaso entrada
'adjustment' -- Ajuste de inventario
);
COMMENT ON TYPE inventory_management.movement_type IS 'Tipos de movimiento de inventario';
-- Origen del movimiento
CREATE TYPE inventory_management.movement_source AS ENUM (
'purchase_order', -- Orden de compra
'transfer', -- Traspaso entre almacenes
'return', -- Devolucion
'adjustment', -- Ajuste
'production', -- Produccion
'consumption' -- Consumo en proyecto
);
COMMENT ON TYPE inventory_management.movement_source IS 'Origen del movimiento de inventario';
-- Estado de traspaso
CREATE TYPE inventory_management.transfer_status AS ENUM (
'pending', -- Pendiente
'in_transit', -- En transito
'received', -- Recibido
'cancelled' -- Cancelado
);
COMMENT ON TYPE inventory_management.transfer_status IS 'Estados de traspaso entre almacenes';
2. Tabla: warehouses
-- ============================================================================
-- Schema: inventory_management
-- Tabla: warehouses
-- Descripcion: Almacenes generales, de proyecto y temporales
-- Modulo: MAI-004
-- ============================================================================
CREATE TABLE IF NOT EXISTS inventory_management.warehouses (
-- Primary Key
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
-- Multi-tenant
tenant_id UUID NOT NULL REFERENCES core_tenants.tenants(id) ON DELETE CASCADE,
-- Identificacion
code VARCHAR(20) NOT NULL,
name VARCHAR(255) NOT NULL,
-- Tipo
type inventory_management.warehouse_type NOT NULL DEFAULT 'general',
-- Relacion con proyecto (solo si type = 'project')
project_id UUID,
-- Ubicacion
address TEXT,
city VARCHAR(100),
state VARCHAR(100),
postal_code VARCHAR(10),
-- Responsable
managed_by UUID REFERENCES core_users.users(id),
-- Dimensiones
total_area DECIMAL(10,2),
covered_area DECIMAL(10,2),
-- Configuracion
settings JSONB DEFAULT '{}',
/* Estructura:
{
allowNegativeStock: boolean,
requiresLocationTracking: boolean,
defaultLocationId: UUID,
autoGenerateLocations: boolean
}
*/
-- Auditoria
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
created_by UUID REFERENCES core_users.users(id),
updated_by UUID REFERENCES core_users.users(id),
-- Soft delete
is_active BOOLEAN NOT NULL DEFAULT true,
deleted_at TIMESTAMPTZ,
deleted_by UUID REFERENCES core_users.users(id),
-- Constraints
CONSTRAINT chk_warehouses_project CHECK (
(type = 'project' AND project_id IS NOT NULL) OR
(type != 'project')
),
CONSTRAINT uq_warehouses_code_tenant UNIQUE (tenant_id, code)
);
COMMENT ON TABLE inventory_management.warehouses IS 'Almacenes generales, de proyecto y temporales';
COMMENT ON COLUMN inventory_management.warehouses.type IS 'Tipo: general, project, temporary';
-- Indices
CREATE INDEX idx_warehouses_tenant_id ON inventory_management.warehouses(tenant_id);
CREATE INDEX idx_warehouses_type ON inventory_management.warehouses(type) WHERE is_active = true;
CREATE INDEX idx_warehouses_project_id ON inventory_management.warehouses(project_id) WHERE project_id IS NOT NULL;
CREATE INDEX idx_warehouses_managed_by ON inventory_management.warehouses(managed_by);
-- RLS Policies
ALTER TABLE inventory_management.warehouses ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation_select ON inventory_management.warehouses
FOR SELECT USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid);
CREATE POLICY tenant_isolation_insert ON inventory_management.warehouses
FOR INSERT WITH CHECK (tenant_id = current_setting('app.current_tenant_id', true)::uuid);
CREATE POLICY tenant_isolation_update ON inventory_management.warehouses
FOR UPDATE USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid);
CREATE POLICY tenant_isolation_delete ON inventory_management.warehouses
FOR DELETE USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid);
3. Tabla: warehouse_locations
-- ============================================================================
-- Schema: inventory_management
-- Tabla: warehouse_locations
-- Descripcion: Ubicaciones dentro de almacenes (ej: A-01, B-03)
-- Modulo: MAI-004
-- ============================================================================
CREATE TABLE IF NOT EXISTS inventory_management.warehouse_locations (
-- Primary Key
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
-- Multi-tenant
tenant_id UUID NOT NULL REFERENCES core_tenants.tenants(id) ON DELETE CASCADE,
-- Relacion
warehouse_id UUID NOT NULL REFERENCES inventory_management.warehouses(id) ON DELETE CASCADE,
-- Identificacion
zone VARCHAR(10) NOT NULL, -- A, B, C, D
position VARCHAR(10) NOT NULL, -- 01, 02, 03
code VARCHAR(20) NOT NULL, -- A-01, B-03
description VARCHAR(255),
capacity_m3 DECIMAL(10,2),
-- Auditoria
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
is_active BOOLEAN NOT NULL DEFAULT true,
-- Constraints
CONSTRAINT uq_warehouse_locations_code UNIQUE (warehouse_id, code)
);
COMMENT ON TABLE inventory_management.warehouse_locations IS 'Ubicaciones fisicas dentro de almacenes';
COMMENT ON COLUMN inventory_management.warehouse_locations.code IS 'Codigo de ubicacion (ej: A-01, B-03)';
-- Indices
CREATE INDEX idx_warehouse_locations_tenant_id ON inventory_management.warehouse_locations(tenant_id);
CREATE INDEX idx_warehouse_locations_warehouse_id ON inventory_management.warehouse_locations(warehouse_id);
CREATE INDEX idx_warehouse_locations_code ON inventory_management.warehouse_locations(code);
-- RLS Policies
ALTER TABLE inventory_management.warehouse_locations ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation_select ON inventory_management.warehouse_locations
FOR SELECT USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid);
CREATE POLICY tenant_isolation_insert ON inventory_management.warehouse_locations
FOR INSERT WITH CHECK (tenant_id = current_setting('app.current_tenant_id', true)::uuid);
CREATE POLICY tenant_isolation_update ON inventory_management.warehouse_locations
FOR UPDATE USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid);
CREATE POLICY tenant_isolation_delete ON inventory_management.warehouse_locations
FOR DELETE USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid);
4. Tabla: inventory_movements
-- ============================================================================
-- Schema: inventory_management
-- Tabla: inventory_movements
-- Descripcion: Movimientos de inventario (entradas, salidas, traspasos)
-- Modulo: MAI-004
-- ============================================================================
CREATE TABLE IF NOT EXISTS inventory_management.inventory_movements (
-- Primary Key
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
-- Multi-tenant
tenant_id UUID NOT NULL REFERENCES core_tenants.tenants(id) ON DELETE CASCADE,
-- Identificacion
code VARCHAR(30) NOT NULL,
-- Almacen
warehouse_id UUID NOT NULL REFERENCES inventory_management.warehouses(id),
-- Tipo de movimiento
type inventory_management.movement_type NOT NULL,
movement_date DATE NOT NULL DEFAULT CURRENT_DATE,
-- Origen del movimiento
source_type inventory_management.movement_source,
source_id UUID,
-- Para salidas a proyecto
project_id UUID,
budget_item_id UUID,
-- Para traspasos
transfer_warehouse_id UUID REFERENCES inventory_management.warehouses(id),
transfer_status inventory_management.transfer_status,
received_at TIMESTAMPTZ,
-- Items
items JSONB NOT NULL,
/* Estructura:
[{
materialId: UUID,
materialCode: string,
description: string,
quantity: number,
unit: string,
unitCost: number,
totalCost: number,
lotId: UUID,
locationId: UUID
}]
*/
total_value DECIMAL(15,2) NOT NULL,
notes TEXT,
attachments TEXT[],
-- Autorizacion
authorized_by UUID REFERENCES core_users.users(id),
recorded_by UUID NOT NULL REFERENCES core_users.users(id),
-- Auditoria
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
created_by UUID REFERENCES core_users.users(id),
-- Constraints
CONSTRAINT chk_inventory_movements_transfer CHECK (
(type IN ('transfer_out', 'transfer_in') AND transfer_warehouse_id IS NOT NULL AND transfer_status IS NOT NULL) OR
(type NOT IN ('transfer_out', 'transfer_in'))
),
CONSTRAINT chk_inventory_movements_exit_project CHECK (
(type = 'exit' AND source_type = 'consumption' AND project_id IS NOT NULL) OR
(type != 'exit' OR source_type != 'consumption')
),
CONSTRAINT uq_inventory_movements_code_tenant UNIQUE (tenant_id, code)
);
COMMENT ON TABLE inventory_management.inventory_movements IS 'Registro de todos los movimientos de inventario';
COMMENT ON COLUMN inventory_management.inventory_movements.type IS 'Tipo: entry, exit, transfer_out, transfer_in, adjustment';
COMMENT ON COLUMN inventory_management.inventory_movements.source_type IS 'Origen: purchase_order, transfer, return, adjustment, production';
-- Indices
CREATE INDEX idx_inventory_movements_tenant_id ON inventory_management.inventory_movements(tenant_id);
CREATE INDEX idx_inventory_movements_warehouse_id ON inventory_management.inventory_movements(warehouse_id);
CREATE INDEX idx_inventory_movements_type ON inventory_management.inventory_movements(type);
CREATE INDEX idx_inventory_movements_date ON inventory_management.inventory_movements(movement_date DESC);
CREATE INDEX idx_inventory_movements_project_id ON inventory_management.inventory_movements(project_id) WHERE project_id IS NOT NULL;
CREATE INDEX idx_inventory_movements_source ON inventory_management.inventory_movements(source_type, source_id);
-- Indice GIN para busqueda en items
CREATE INDEX idx_inventory_movements_items_gin ON inventory_management.inventory_movements USING gin (items jsonb_path_ops);
-- RLS Policies
ALTER TABLE inventory_management.inventory_movements ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation_select ON inventory_management.inventory_movements
FOR SELECT USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid);
CREATE POLICY tenant_isolation_insert ON inventory_management.inventory_movements
FOR INSERT WITH CHECK (tenant_id = current_setting('app.current_tenant_id', true)::uuid);
CREATE POLICY tenant_isolation_update ON inventory_management.inventory_movements
FOR UPDATE USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid);
CREATE POLICY tenant_isolation_delete ON inventory_management.inventory_movements
FOR DELETE USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid);
5. Tabla: stock_levels
-- ============================================================================
-- Schema: inventory_management
-- Tabla: stock_levels
-- Descripcion: Niveles de stock actuales por almacen y material
-- Modulo: MAI-004
-- ============================================================================
CREATE TABLE IF NOT EXISTS inventory_management.stock_levels (
-- Primary Key
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
-- Multi-tenant
tenant_id UUID NOT NULL REFERENCES core_tenants.tenants(id) ON DELETE CASCADE,
-- Relaciones
warehouse_id UUID NOT NULL REFERENCES inventory_management.warehouses(id),
material_id UUID NOT NULL, -- FK a core_catalogs.contacts o materials
location_id UUID REFERENCES inventory_management.warehouse_locations(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,
-- Valoracion
average_cost DECIMAL(12,2) NOT NULL DEFAULT 0,
total_value DECIMAL(15,2) GENERATED ALWAYS AS (quantity * average_cost) STORED,
-- Fechas de ultimo movimiento
last_movement_date DATE,
last_entry_date DATE,
last_exit_date DATE,
-- Auditoria
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
-- Constraints
CONSTRAINT chk_stock_levels_quantity CHECK (quantity >= 0),
CONSTRAINT chk_stock_levels_reserved CHECK (reserved_quantity >= 0 AND reserved_quantity <= quantity),
CONSTRAINT uq_stock_levels_warehouse_material UNIQUE (warehouse_id, material_id)
);
COMMENT ON TABLE inventory_management.stock_levels IS 'Niveles de stock actuales por almacen y material';
COMMENT ON COLUMN inventory_management.stock_levels.quantity IS 'Cantidad total en almacen';
COMMENT ON COLUMN inventory_management.stock_levels.reserved_quantity IS 'Cantidad reservada para salidas';
COMMENT ON COLUMN inventory_management.stock_levels.available_quantity IS 'Cantidad disponible (quantity - reserved_quantity)';
COMMENT ON COLUMN inventory_management.stock_levels.average_cost IS 'Costo promedio ponderado';
-- Indices
CREATE INDEX idx_stock_levels_tenant_id ON inventory_management.stock_levels(tenant_id);
CREATE INDEX idx_stock_levels_warehouse_id ON inventory_management.stock_levels(warehouse_id);
CREATE INDEX idx_stock_levels_material_id ON inventory_management.stock_levels(material_id);
CREATE INDEX idx_stock_levels_location_id ON inventory_management.stock_levels(location_id) WHERE location_id IS NOT NULL;
CREATE INDEX idx_stock_levels_last_movement ON inventory_management.stock_levels(last_movement_date DESC);
-- Indice para buscar stock bajo
CREATE INDEX idx_stock_levels_quantity ON inventory_management.stock_levels(quantity) WHERE quantity > 0;
-- RLS Policies
ALTER TABLE inventory_management.stock_levels ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation_select ON inventory_management.stock_levels
FOR SELECT USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid);
CREATE POLICY tenant_isolation_insert ON inventory_management.stock_levels
FOR INSERT WITH CHECK (tenant_id = current_setting('app.current_tenant_id', true)::uuid);
CREATE POLICY tenant_isolation_update ON inventory_management.stock_levels
FOR UPDATE USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid);
CREATE POLICY tenant_isolation_delete ON inventory_management.stock_levels
FOR DELETE USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid);
6. Tabla: inventory_lots
-- ============================================================================
-- Schema: inventory_management
-- Tabla: inventory_lots
-- Descripcion: Lotes de inventario para valoracion PEPS
-- Modulo: MAI-004
-- ============================================================================
CREATE TABLE IF NOT EXISTS inventory_management.inventory_lots (
-- Primary Key
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
-- Multi-tenant
tenant_id UUID NOT NULL REFERENCES core_tenants.tenants(id) ON DELETE CASCADE,
-- Relaciones
warehouse_id UUID NOT NULL REFERENCES inventory_management.warehouses(id),
material_id UUID NOT NULL,
-- Identificacion del lote
lot_number VARCHAR(50),
entry_date DATE NOT NULL DEFAULT CURRENT_DATE,
-- Cantidades
quantity DECIMAL(12,4) NOT NULL,
remaining_quantity DECIMAL(12,4) NOT NULL,
unit_cost DECIMAL(12,2) NOT NULL,
-- Origen
source_type inventory_management.movement_source NOT NULL,
source_id UUID NOT NULL,
-- Estado
is_depleted BOOLEAN GENERATED ALWAYS AS (remaining_quantity <= 0.0001) STORED,
-- Auditoria
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
-- Constraints
CONSTRAINT chk_inventory_lots_remaining CHECK (remaining_quantity >= 0 AND remaining_quantity <= quantity),
CONSTRAINT chk_inventory_lots_unit_cost CHECK (unit_cost >= 0)
);
COMMENT ON TABLE inventory_management.inventory_lots IS 'Lotes de inventario para valoracion PEPS (Primeras Entradas, Primeras Salidas)';
COMMENT ON COLUMN inventory_management.inventory_lots.lot_number IS 'Numero de lote del proveedor (opcional)';
COMMENT ON COLUMN inventory_management.inventory_lots.remaining_quantity IS 'Cantidad restante del lote';
COMMENT ON COLUMN inventory_management.inventory_lots.is_depleted IS 'Si el lote se agoto completamente';
-- Indices
CREATE INDEX idx_inventory_lots_tenant_id ON inventory_management.inventory_lots(tenant_id);
CREATE INDEX idx_inventory_lots_warehouse_material ON inventory_management.inventory_lots(warehouse_id, material_id, entry_date);
CREATE INDEX idx_inventory_lots_not_depleted ON inventory_management.inventory_lots(warehouse_id, material_id, entry_date)
WHERE is_depleted = false;
CREATE INDEX idx_inventory_lots_source ON inventory_management.inventory_lots(source_type, source_id);
-- RLS Policies
ALTER TABLE inventory_management.inventory_lots ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation_select ON inventory_management.inventory_lots
FOR SELECT USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid);
CREATE POLICY tenant_isolation_insert ON inventory_management.inventory_lots
FOR INSERT WITH CHECK (tenant_id = current_setting('app.current_tenant_id', true)::uuid);
CREATE POLICY tenant_isolation_update ON inventory_management.inventory_lots
FOR UPDATE USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid);
CREATE POLICY tenant_isolation_delete ON inventory_management.inventory_lots
FOR DELETE USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid);
Funciones y Triggers
1. Trigger: updated_at
-- ============================================================================
-- TRIGGER: Actualizar updated_at automaticamente
-- ============================================================================
-- Aplicar a tablas con updated_at
CREATE TRIGGER trg_requisitions_update_timestamp
BEFORE UPDATE ON purchasing_management.requisitions
FOR EACH ROW
EXECUTE FUNCTION core_shared.set_updated_at();
CREATE TRIGGER trg_rfqs_update_timestamp
BEFORE UPDATE ON purchasing_management.rfqs
FOR EACH ROW
EXECUTE FUNCTION core_shared.set_updated_at();
CREATE TRIGGER trg_quotations_update_timestamp
BEFORE UPDATE ON purchasing_management.quotations
FOR EACH ROW
EXECUTE FUNCTION core_shared.set_updated_at();
CREATE TRIGGER trg_purchase_orders_update_timestamp
BEFORE UPDATE ON purchasing_management.purchase_orders
FOR EACH ROW
EXECUTE FUNCTION core_shared.set_updated_at();
CREATE TRIGGER trg_warehouses_update_timestamp
BEFORE UPDATE ON inventory_management.warehouses
FOR EACH ROW
EXECUTE FUNCTION core_shared.set_updated_at();
CREATE TRIGGER trg_stock_levels_update_timestamp
BEFORE UPDATE ON inventory_management.stock_levels
FOR EACH ROW
EXECUTE FUNCTION core_shared.set_updated_at();
2. Trigger: Actualizar estado de OC al recibir material
-- ============================================================================
-- TRIGGER: Actualizar estado de orden de compra segun recepciones
-- ============================================================================
CREATE OR REPLACE FUNCTION purchasing_management.update_po_status_on_receipt()
RETURNS TRIGGER AS $$
DECLARE
v_total_ordered DECIMAL(15,4);
v_total_received DECIMAL(15,4);
v_po_item JSONB;
v_receipt_item JSONB;
BEGIN
-- Calcular total ordenado
SELECT COALESCE(SUM((item->>'quantity')::DECIMAL), 0)
INTO v_total_ordered
FROM purchasing_management.purchase_orders po,
JSONB_ARRAY_ELEMENTS(po.items) AS item
WHERE po.id = NEW.purchase_order_id;
-- Calcular total recibido (aceptado) de todas las recepciones
SELECT COALESCE(SUM((item->>'acceptedQuantity')::DECIMAL), 0)
INTO v_total_received
FROM purchasing_management.po_receipts pr,
JSONB_ARRAY_ELEMENTS(pr.items) AS item
WHERE pr.purchase_order_id = NEW.purchase_order_id;
-- Actualizar estado de la OC
IF v_total_received >= v_total_ordered THEN
UPDATE purchasing_management.purchase_orders
SET status = 'received',
updated_at = now()
WHERE id = NEW.purchase_order_id
AND status != 'received';
ELSIF v_total_received > 0 THEN
UPDATE purchasing_management.purchase_orders
SET status = 'partially_received',
updated_at = now()
WHERE id = NEW.purchase_order_id
AND status NOT IN ('received', 'partially_received');
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER trg_po_receipts_update_po_status
AFTER INSERT ON purchasing_management.po_receipts
FOR EACH ROW
EXECUTE FUNCTION purchasing_management.update_po_status_on_receipt();
3. Funcion: Actualizacion de stock y creacion de lotes
-- ============================================================================
-- FUNCION: Actualizar stock y crear lotes PEPS al recibir material
-- ============================================================================
CREATE OR REPLACE FUNCTION inventory_management.update_stock_on_entry(
p_tenant_id UUID,
p_warehouse_id UUID,
p_material_id UUID,
p_quantity DECIMAL(12,4),
p_unit_cost DECIMAL(12,2),
p_source_type inventory_management.movement_source,
p_source_id UUID,
p_lot_number VARCHAR(50) DEFAULT NULL
)
RETURNS VOID AS $$
DECLARE
v_current_quantity DECIMAL(12,4);
v_current_value DECIMAL(15,2);
v_new_quantity DECIMAL(12,4);
v_new_average_cost DECIMAL(12,2);
BEGIN
-- Obtener stock actual
SELECT quantity, total_value
INTO v_current_quantity, v_current_value
FROM inventory_management.stock_levels
WHERE warehouse_id = p_warehouse_id
AND material_id = p_material_id;
-- Si no existe, crear registro de stock
IF NOT FOUND THEN
INSERT INTO inventory_management.stock_levels (
tenant_id, warehouse_id, material_id,
quantity, reserved_quantity, average_cost,
last_entry_date, last_movement_date
) VALUES (
p_tenant_id, p_warehouse_id, p_material_id,
p_quantity, 0, p_unit_cost,
CURRENT_DATE, CURRENT_DATE
);
ELSE
-- Calcular nueva cantidad y costo promedio
v_new_quantity := v_current_quantity + p_quantity;
v_new_average_cost := (v_current_value + (p_quantity * p_unit_cost)) / v_new_quantity;
-- Actualizar stock
UPDATE inventory_management.stock_levels
SET quantity = v_new_quantity,
average_cost = v_new_average_cost,
last_entry_date = CURRENT_DATE,
last_movement_date = CURRENT_DATE,
updated_at = now()
WHERE warehouse_id = p_warehouse_id
AND material_id = p_material_id;
END IF;
-- Crear lote PEPS
INSERT INTO inventory_management.inventory_lots (
tenant_id, warehouse_id, material_id,
lot_number, entry_date,
quantity, remaining_quantity, unit_cost,
source_type, source_id
) VALUES (
p_tenant_id, p_warehouse_id, p_material_id,
p_lot_number, CURRENT_DATE,
p_quantity, p_quantity, p_unit_cost,
p_source_type, p_source_id
);
END;
$$ LANGUAGE plpgsql;
COMMENT ON FUNCTION inventory_management.update_stock_on_entry IS 'Actualiza stock y crea lote PEPS al recibir material';
4. Funcion: Valoracion PEPS en salidas
-- ============================================================================
-- FUNCION: Calcular costo PEPS en salidas de inventario
-- ============================================================================
CREATE OR REPLACE FUNCTION inventory_management.calculate_fifo_cost(
p_warehouse_id UUID,
p_material_id UUID,
p_quantity DECIMAL(12,4)
)
RETURNS TABLE (
lot_id UUID,
quantity_consumed DECIMAL(12,4),
unit_cost DECIMAL(12,2),
total_cost DECIMAL(15,2)
) AS $$
DECLARE
v_remaining DECIMAL(12,4) := p_quantity;
v_lot RECORD;
v_consume_qty DECIMAL(12,4);
BEGIN
-- Iterar lotes en orden FIFO (primero el mas antiguo)
FOR v_lot IN
SELECT id, remaining_quantity, unit_cost
FROM inventory_management.inventory_lots
WHERE warehouse_id = p_warehouse_id
AND material_id = p_material_id
AND is_depleted = false
ORDER BY entry_date ASC, created_at ASC
LOOP
EXIT WHEN v_remaining <= 0;
-- Calcular cuanto consumir de este lote
v_consume_qty := LEAST(v_lot.remaining_quantity, v_remaining);
-- Retornar informacion del consumo
lot_id := v_lot.id;
quantity_consumed := v_consume_qty;
unit_cost := v_lot.unit_cost;
total_cost := v_consume_qty * v_lot.unit_cost;
RETURN NEXT;
v_remaining := v_remaining - v_consume_qty;
END LOOP;
-- Verificar si hay stock suficiente
IF v_remaining > 0.0001 THEN
RAISE EXCEPTION 'Stock insuficiente. Faltan % unidades', v_remaining;
END IF;
END;
$$ LANGUAGE plpgsql;
COMMENT ON FUNCTION inventory_management.calculate_fifo_cost IS 'Calcula costo PEPS y distribucion por lotes para una salida';
5. Funcion: Salida de inventario con PEPS
-- ============================================================================
-- FUNCION: Procesar salida de inventario con valoracion PEPS
-- ============================================================================
CREATE OR REPLACE FUNCTION inventory_management.update_stock_on_exit(
p_tenant_id UUID,
p_warehouse_id UUID,
p_material_id UUID,
p_quantity DECIMAL(12,4)
)
RETURNS TABLE (
total_cost DECIMAL(15,2),
average_cost DECIMAL(12,2)
) AS $$
DECLARE
v_fifo_record RECORD;
v_total_cost DECIMAL(15,2) := 0;
v_avg_cost DECIMAL(12,2);
BEGIN
-- Verificar stock disponible
IF NOT EXISTS (
SELECT 1
FROM inventory_management.stock_levels
WHERE warehouse_id = p_warehouse_id
AND material_id = p_material_id
AND available_quantity >= p_quantity
) THEN
RAISE EXCEPTION 'Stock insuficiente disponible para material %', p_material_id;
END IF;
-- Consumir lotes PEPS
FOR v_fifo_record IN
SELECT *
FROM inventory_management.calculate_fifo_cost(p_warehouse_id, p_material_id, p_quantity)
LOOP
-- Actualizar lote
UPDATE inventory_management.inventory_lots
SET remaining_quantity = remaining_quantity - v_fifo_record.quantity_consumed
WHERE id = v_fifo_record.lot_id;
v_total_cost := v_total_cost + v_fifo_record.total_cost;
END LOOP;
-- Calcular costo promedio
v_avg_cost := v_total_cost / p_quantity;
-- Actualizar stock
UPDATE inventory_management.stock_levels
SET quantity = quantity - p_quantity,
last_exit_date = CURRENT_DATE,
last_movement_date = CURRENT_DATE,
updated_at = now()
WHERE warehouse_id = p_warehouse_id
AND material_id = p_material_id;
total_cost := v_total_cost;
average_cost := v_avg_cost;
RETURN NEXT;
END;
$$ LANGUAGE plpgsql;
COMMENT ON FUNCTION inventory_management.update_stock_on_exit IS 'Procesa salida de inventario con valoracion PEPS';
6. Funcion: Generar codigo de requisicion
-- ============================================================================
-- FUNCION: Generar codigo automatico de requisicion
-- ============================================================================
CREATE OR REPLACE FUNCTION purchasing_management.generate_requisition_code()
RETURNS TRIGGER AS $$
DECLARE
v_year VARCHAR(4);
v_sequence INT;
v_code VARCHAR(30);
BEGIN
IF NEW.code IS NOT NULL AND NEW.code != '' THEN
RETURN NEW;
END IF;
v_year := TO_CHAR(CURRENT_DATE, 'YYYY');
SELECT COALESCE(MAX(SUBSTRING(code FROM 10)::INT), 0) + 1
INTO v_sequence
FROM purchasing_management.requisitions
WHERE tenant_id = NEW.tenant_id
AND code LIKE 'REQ-' || v_year || '-%';
v_code := 'REQ-' || v_year || '-' || LPAD(v_sequence::TEXT, 5, '0');
NEW.code := v_code;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER trg_requisitions_generate_code
BEFORE INSERT ON purchasing_management.requisitions
FOR EACH ROW
EXECUTE FUNCTION purchasing_management.generate_requisition_code();
7. Funcion: Generar codigos para otras entidades
-- ============================================================================
-- FUNCIONES: Generar codigos automaticos
-- ============================================================================
-- RFQ
CREATE OR REPLACE FUNCTION purchasing_management.generate_rfq_code()
RETURNS TRIGGER AS $$
DECLARE
v_year VARCHAR(4);
v_sequence INT;
BEGIN
IF NEW.code IS NOT NULL AND NEW.code != '' THEN
RETURN NEW;
END IF;
v_year := TO_CHAR(CURRENT_DATE, 'YYYY');
SELECT COALESCE(MAX(SUBSTRING(code FROM 10)::INT), 0) + 1
INTO v_sequence
FROM purchasing_management.rfqs
WHERE tenant_id = NEW.tenant_id
AND code LIKE 'RFQ-' || v_year || '-%';
NEW.code := 'RFQ-' || v_year || '-' || LPAD(v_sequence::TEXT, 5, '0');
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER trg_rfqs_generate_code
BEFORE INSERT ON purchasing_management.rfqs
FOR EACH ROW
EXECUTE FUNCTION purchasing_management.generate_rfq_code();
-- Quotation
CREATE OR REPLACE FUNCTION purchasing_management.generate_quotation_code()
RETURNS TRIGGER AS $$
DECLARE
v_year VARCHAR(4);
v_sequence INT;
BEGIN
IF NEW.code IS NOT NULL AND NEW.code != '' THEN
RETURN NEW;
END IF;
v_year := TO_CHAR(CURRENT_DATE, 'YYYY');
SELECT COALESCE(MAX(SUBSTRING(code FROM 11)::INT), 0) + 1
INTO v_sequence
FROM purchasing_management.quotations
WHERE tenant_id = NEW.tenant_id
AND code LIKE 'QUOT-' || v_year || '-%';
NEW.code := 'QUOT-' || v_year || '-' || LPAD(v_sequence::TEXT, 5, '0');
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER trg_quotations_generate_code
BEFORE INSERT ON purchasing_management.quotations
FOR EACH ROW
EXECUTE FUNCTION purchasing_management.generate_quotation_code();
-- Purchase Order
CREATE OR REPLACE FUNCTION purchasing_management.generate_po_code()
RETURNS TRIGGER AS $$
DECLARE
v_year VARCHAR(4);
v_sequence INT;
BEGIN
IF NEW.code IS NOT NULL AND NEW.code != '' THEN
RETURN NEW;
END IF;
v_year := TO_CHAR(CURRENT_DATE, 'YYYY');
SELECT COALESCE(MAX(SUBSTRING(code FROM 9)::INT), 0) + 1
INTO v_sequence
FROM purchasing_management.purchase_orders
WHERE tenant_id = NEW.tenant_id
AND code LIKE 'OC-' || v_year || '-%';
NEW.code := 'OC-' || v_year || '-' || LPAD(v_sequence::TEXT, 5, '0');
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER trg_purchase_orders_generate_code
BEFORE INSERT ON purchasing_management.purchase_orders
FOR EACH ROW
EXECUTE FUNCTION purchasing_management.generate_po_code();
-- PO Receipt
CREATE OR REPLACE FUNCTION purchasing_management.generate_po_receipt_code()
RETURNS TRIGGER AS $$
DECLARE
v_year VARCHAR(4);
v_sequence INT;
BEGIN
IF NEW.code IS NOT NULL AND NEW.code != '' THEN
RETURN NEW;
END IF;
v_year := TO_CHAR(CURRENT_DATE, 'YYYY');
SELECT COALESCE(MAX(SUBSTRING(code FROM 10)::INT), 0) + 1
INTO v_sequence
FROM purchasing_management.po_receipts
WHERE tenant_id = NEW.tenant_id
AND code LIKE 'REC-' || v_year || '-%';
NEW.code := 'REC-' || v_year || '-' || LPAD(v_sequence::TEXT, 5, '0');
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER trg_po_receipts_generate_code
BEFORE INSERT ON purchasing_management.po_receipts
FOR EACH ROW
EXECUTE FUNCTION purchasing_management.generate_po_receipt_code();
-- Inventory Movement
CREATE OR REPLACE FUNCTION inventory_management.generate_movement_code()
RETURNS TRIGGER AS $$
DECLARE
v_year VARCHAR(4);
v_sequence INT;
v_prefix VARCHAR(5);
BEGIN
IF NEW.code IS NOT NULL AND NEW.code != '' THEN
RETURN NEW;
END IF;
v_year := TO_CHAR(CURRENT_DATE, 'YYYY');
-- Prefijo segun tipo de movimiento
v_prefix := CASE NEW.type
WHEN 'entry' THEN 'ENT'
WHEN 'exit' THEN 'SAL'
WHEN 'transfer_out' THEN 'TSO'
WHEN 'transfer_in' THEN 'TSI'
WHEN 'adjustment' THEN 'AJU'
ELSE 'MOV'
END;
SELECT COALESCE(MAX(SUBSTRING(code FROM LENGTH(v_prefix) + 7)::INT), 0) + 1
INTO v_sequence
FROM inventory_management.inventory_movements
WHERE tenant_id = NEW.tenant_id
AND code LIKE v_prefix || '-' || v_year || '-%';
NEW.code := v_prefix || '-' || v_year || '-' || LPAD(v_sequence::TEXT, 5, '0');
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER trg_inventory_movements_generate_code
BEFORE INSERT ON inventory_management.inventory_movements
FOR EACH ROW
EXECUTE FUNCTION inventory_management.generate_movement_code();
Vistas Utiles
1. Vista: Stock consolidado por material
-- ============================================================================
-- VISTA: Stock consolidado por material (todos los almacenes)
-- ============================================================================
CREATE OR REPLACE VIEW inventory_management.v_stock_consolidated AS
SELECT
tenant_id,
material_id,
SUM(quantity) AS total_quantity,
SUM(reserved_quantity) AS total_reserved,
SUM(available_quantity) AS total_available,
AVG(average_cost) AS weighted_average_cost,
SUM(total_value) AS total_value,
COUNT(DISTINCT warehouse_id) AS warehouses_count,
MAX(last_movement_date) AS last_movement_date
FROM inventory_management.stock_levels
WHERE quantity > 0
GROUP BY tenant_id, material_id;
COMMENT ON VIEW inventory_management.v_stock_consolidated IS 'Stock consolidado por material sumando todos los almacenes';
2. Vista: Ordenes de compra pendientes de recepcion
-- ============================================================================
-- VISTA: Ordenes de compra pendientes de recepcion
-- ============================================================================
CREATE OR REPLACE VIEW purchasing_management.v_pending_receipts AS
SELECT
po.id,
po.code,
po.tenant_id,
po.supplier_id,
po.project_id,
po.order_date,
po.delivery_date,
po.status,
po.total,
CURRENT_DATE - po.delivery_date AS days_overdue,
CASE
WHEN CURRENT_DATE > po.delivery_date THEN true
ELSE false
END AS is_overdue
FROM purchasing_management.purchase_orders po
WHERE po.status IN ('sent', 'partially_received')
AND po.is_active = true
ORDER BY po.delivery_date ASC;
COMMENT ON VIEW purchasing_management.v_pending_receipts IS 'Ordenes de compra pendientes de recibir (total o parcialmente)';
Scripts de Inicializacion
Schema creation
-- ============================================================================
-- CREAR SCHEMAS
-- ============================================================================
CREATE SCHEMA IF NOT EXISTS purchasing_management;
CREATE SCHEMA IF NOT EXISTS inventory_management;
COMMENT ON SCHEMA purchasing_management IS 'Gestion de compras: requisiciones, cotizaciones, ordenes';
COMMENT ON SCHEMA inventory_management IS 'Gestion de inventarios: almacenes, movimientos, stock';
Notas de Implementacion
Orden de Creacion
- Crear schemas
- Crear ENUMs
- Crear tablas en orden:
- purchasing_management.requisitions
- purchasing_management.rfqs
- purchasing_management.quotations
- purchasing_management.purchase_orders
- purchasing_management.po_receipts
- inventory_management.warehouses
- inventory_management.warehouse_locations
- inventory_management.inventory_movements
- inventory_management.stock_levels
- inventory_management.inventory_lots
- Crear indices
- Habilitar RLS y crear policies
- Crear funciones
- Crear triggers
- Crear vistas
Dependencias Externas
core_tenants.tenants- Tabla de tenantscore_users.users- Tabla de usuarioscore_shared.set_updated_at()- Funcion compartida para updated_at- Schema
projects- Para relacion con proyectos - Schema
core_catalogs- Para materiales y proveedores
Consideraciones de Rendimiento
- Indices GIN en columnas JSONB para busquedas eficientes
- Indices parciales en columnas de estado para queries frecuentes
- Columnas generadas (STORED) para calculos frecuentes
- Particionamiento futuro en
inventory_movementspor fecha si crece mucho
Migraciones
- Crear migracion TypeORM siguiendo nomenclatura:
{timestamp}-create-purchasing-inventory-schemas.ts - Separar en multiples archivos si es muy grande
- Incluir rollback (down) para todas las operaciones
Estado: En Diseno Proximos Pasos: Revision y aprobacion para implementacion Fecha Creacion: 2025-12-06