🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
915 lines
31 KiB
PL/PgSQL
915 lines
31 KiB
PL/PgSQL
-- =====================================================
|
|
-- SCHEMA: purchase
|
|
-- PROPÓSITO: Gestión de compras, proveedores, órdenes de compra
|
|
-- MÓDULOS: MGN-006 (Compras Básico)
|
|
-- FECHA: 2025-11-24
|
|
-- =====================================================
|
|
|
|
-- Crear schema
|
|
CREATE SCHEMA IF NOT EXISTS purchase;
|
|
|
|
-- =====================================================
|
|
-- TYPES (ENUMs)
|
|
-- =====================================================
|
|
|
|
CREATE TYPE purchase.order_status AS ENUM (
|
|
'draft',
|
|
'sent',
|
|
'to_approve', -- COR-001: Estado de aprobación (Odoo alignment)
|
|
'purchase', -- COR-001: Renombrado de 'confirmed' para alinear con Odoo
|
|
'received',
|
|
'billed',
|
|
'cancelled'
|
|
);
|
|
|
|
CREATE TYPE purchase.rfq_status AS ENUM (
|
|
'draft',
|
|
'sent',
|
|
'responded',
|
|
'accepted',
|
|
'rejected',
|
|
'cancelled'
|
|
);
|
|
|
|
CREATE TYPE purchase.agreement_type AS ENUM (
|
|
'price',
|
|
'discount',
|
|
'blanket'
|
|
);
|
|
|
|
-- =====================================================
|
|
-- TABLES
|
|
-- =====================================================
|
|
|
|
-- Tabla: purchase_orders (Órdenes de compra)
|
|
CREATE TABLE purchase.purchase_orders (
|
|
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,
|
|
|
|
-- Numeración
|
|
name VARCHAR(100) NOT NULL,
|
|
ref VARCHAR(100), -- Referencia del proveedor
|
|
|
|
-- Proveedor
|
|
partner_id UUID NOT NULL REFERENCES core.partners(id),
|
|
|
|
-- Fechas
|
|
order_date DATE NOT NULL,
|
|
expected_date DATE,
|
|
effective_date DATE,
|
|
|
|
-- Configuración
|
|
currency_id UUID NOT NULL REFERENCES core.currencies(id),
|
|
payment_term_id UUID REFERENCES financial.payment_terms(id),
|
|
|
|
-- Montos
|
|
amount_untaxed DECIMAL(15, 2) NOT NULL DEFAULT 0,
|
|
amount_tax DECIMAL(15, 2) NOT NULL DEFAULT 0,
|
|
amount_total DECIMAL(15, 2) NOT NULL DEFAULT 0,
|
|
|
|
-- Estado
|
|
status purchase.order_status NOT NULL DEFAULT 'draft',
|
|
|
|
-- Recepciones y facturación
|
|
receipt_status VARCHAR(20) DEFAULT 'pending', -- pending, partial, received
|
|
invoice_status VARCHAR(20) DEFAULT 'pending', -- pending, partial, billed
|
|
|
|
-- Relaciones
|
|
picking_id UUID REFERENCES inventory.pickings(id), -- Recepción generada
|
|
invoice_id UUID REFERENCES financial.invoices(id), -- Factura generada
|
|
|
|
-- Notas
|
|
notes TEXT,
|
|
|
|
-- COR-010: Dirección de envío (dropship)
|
|
dest_address_id UUID REFERENCES core.partners(id),
|
|
|
|
-- COR-011: Bloqueo de orden
|
|
locked BOOLEAN DEFAULT FALSE,
|
|
|
|
-- COR-001: Campos de aprobación
|
|
approval_required BOOLEAN DEFAULT FALSE,
|
|
amount_approval_threshold DECIMAL(15, 2),
|
|
|
|
-- 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),
|
|
confirmed_at TIMESTAMP,
|
|
confirmed_by UUID REFERENCES auth.users(id),
|
|
approved_at TIMESTAMP, -- COR-001
|
|
approved_by UUID REFERENCES auth.users(id), -- COR-001
|
|
cancelled_at TIMESTAMP,
|
|
cancelled_by UUID REFERENCES auth.users(id),
|
|
|
|
CONSTRAINT uq_purchase_orders_name_company UNIQUE (company_id, name)
|
|
);
|
|
|
|
-- Tabla: purchase_order_lines (Líneas de orden de compra)
|
|
CREATE TABLE purchase.purchase_order_lines (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE,
|
|
order_id UUID NOT NULL REFERENCES purchase.purchase_orders(id) ON DELETE CASCADE,
|
|
|
|
product_id UUID NOT NULL REFERENCES inventory.products(id),
|
|
description TEXT NOT NULL,
|
|
|
|
-- Cantidades
|
|
quantity DECIMAL(12, 4) NOT NULL,
|
|
qty_received DECIMAL(12, 4) DEFAULT 0,
|
|
qty_invoiced DECIMAL(12, 4) DEFAULT 0,
|
|
uom_id UUID NOT NULL REFERENCES core.uom(id),
|
|
|
|
-- Precios
|
|
price_unit DECIMAL(15, 4) NOT NULL,
|
|
discount DECIMAL(5, 2) DEFAULT 0, -- Porcentaje de descuento
|
|
|
|
-- Impuestos
|
|
tax_ids UUID[] DEFAULT '{}',
|
|
|
|
-- Montos
|
|
amount_untaxed DECIMAL(15, 2) NOT NULL,
|
|
amount_tax DECIMAL(15, 2) NOT NULL,
|
|
amount_total DECIMAL(15, 2) NOT NULL,
|
|
|
|
-- Fechas esperadas
|
|
expected_date DATE,
|
|
|
|
-- 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,
|
|
updated_at TIMESTAMP,
|
|
|
|
CONSTRAINT chk_purchase_order_lines_quantity CHECK (quantity > 0),
|
|
CONSTRAINT chk_purchase_order_lines_discount CHECK (discount >= 0 AND discount <= 100)
|
|
);
|
|
|
|
-- Índices para purchase_order_lines
|
|
CREATE INDEX idx_purchase_order_lines_tenant_id ON purchase.purchase_order_lines(tenant_id);
|
|
CREATE INDEX idx_purchase_order_lines_order_id ON purchase.purchase_order_lines(order_id);
|
|
CREATE INDEX idx_purchase_order_lines_product_id ON purchase.purchase_order_lines(product_id);
|
|
|
|
-- RLS para purchase_order_lines
|
|
ALTER TABLE purchase.purchase_order_lines ENABLE ROW LEVEL SECURITY;
|
|
CREATE POLICY tenant_isolation_purchase_order_lines ON purchase.purchase_order_lines
|
|
USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid);
|
|
|
|
-- Tabla: rfqs (Request for Quotation - Solicitudes de cotización)
|
|
CREATE TABLE purchase.rfqs (
|
|
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,
|
|
|
|
-- Proveedores (puede ser enviada a múltiples proveedores)
|
|
partner_ids UUID[] NOT NULL,
|
|
|
|
-- Fechas
|
|
request_date DATE NOT NULL,
|
|
deadline_date DATE,
|
|
response_date DATE,
|
|
|
|
-- Estado
|
|
status purchase.rfq_status NOT NULL DEFAULT 'draft',
|
|
|
|
-- Descripción
|
|
description TEXT,
|
|
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),
|
|
|
|
CONSTRAINT uq_rfqs_name_company UNIQUE (company_id, name)
|
|
);
|
|
|
|
-- Tabla: rfq_lines (Líneas de RFQ)
|
|
CREATE TABLE purchase.rfq_lines (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE,
|
|
rfq_id UUID NOT NULL REFERENCES purchase.rfqs(id) ON DELETE CASCADE,
|
|
|
|
product_id UUID REFERENCES inventory.products(id),
|
|
description TEXT NOT NULL,
|
|
quantity DECIMAL(12, 4) NOT NULL,
|
|
uom_id UUID NOT NULL REFERENCES core.uom(id),
|
|
|
|
-- Auditoría
|
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
|
|
CONSTRAINT chk_rfq_lines_quantity CHECK (quantity > 0)
|
|
);
|
|
|
|
-- Índices para rfq_lines
|
|
CREATE INDEX idx_rfq_lines_tenant_id ON purchase.rfq_lines(tenant_id);
|
|
|
|
-- RLS para rfq_lines
|
|
ALTER TABLE purchase.rfq_lines ENABLE ROW LEVEL SECURITY;
|
|
CREATE POLICY tenant_isolation_rfq_lines ON purchase.rfq_lines
|
|
USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid);
|
|
|
|
-- Tabla: vendor_pricelists (Listas de precios de proveedores)
|
|
CREATE TABLE purchase.vendor_pricelists (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE,
|
|
|
|
partner_id UUID NOT NULL REFERENCES core.partners(id),
|
|
product_id UUID NOT NULL REFERENCES inventory.products(id),
|
|
|
|
-- Precio
|
|
price DECIMAL(15, 4) NOT NULL,
|
|
currency_id UUID NOT NULL REFERENCES core.currencies(id),
|
|
|
|
-- Cantidad mínima
|
|
min_quantity DECIMAL(12, 4) DEFAULT 1,
|
|
|
|
-- Validez
|
|
valid_from DATE,
|
|
valid_to DATE,
|
|
|
|
-- 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_vendor_pricelists_price CHECK (price >= 0),
|
|
CONSTRAINT chk_vendor_pricelists_min_qty CHECK (min_quantity > 0),
|
|
CONSTRAINT chk_vendor_pricelists_dates CHECK (valid_to IS NULL OR valid_to >= valid_from)
|
|
);
|
|
|
|
-- Tabla: purchase_agreements (Acuerdos de compra / Contratos)
|
|
CREATE TABLE purchase.purchase_agreements (
|
|
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(50),
|
|
agreement_type purchase.agreement_type NOT NULL,
|
|
|
|
-- Proveedor
|
|
partner_id UUID NOT NULL REFERENCES core.partners(id),
|
|
|
|
-- Vigencia
|
|
start_date DATE NOT NULL,
|
|
end_date DATE NOT NULL,
|
|
|
|
-- Montos (para contratos blanket)
|
|
amount_max DECIMAL(15, 2),
|
|
currency_id UUID REFERENCES core.currencies(id),
|
|
|
|
-- Estado
|
|
is_active BOOLEAN DEFAULT TRUE,
|
|
|
|
-- Términos
|
|
terms TEXT,
|
|
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),
|
|
|
|
CONSTRAINT uq_purchase_agreements_code_company UNIQUE (company_id, code),
|
|
CONSTRAINT chk_purchase_agreements_dates CHECK (end_date > start_date)
|
|
);
|
|
|
|
-- Tabla: purchase_agreement_lines (Líneas de acuerdo)
|
|
CREATE TABLE purchase.purchase_agreement_lines (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE,
|
|
agreement_id UUID NOT NULL REFERENCES purchase.purchase_agreements(id) ON DELETE CASCADE,
|
|
|
|
product_id UUID NOT NULL REFERENCES inventory.products(id),
|
|
|
|
-- Cantidades
|
|
quantity DECIMAL(12, 4),
|
|
qty_ordered DECIMAL(12, 4) DEFAULT 0,
|
|
|
|
-- Precio acordado
|
|
price_unit DECIMAL(15, 4),
|
|
discount DECIMAL(5, 2) DEFAULT 0,
|
|
|
|
-- Auditoría
|
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
|
|
-- Índices para purchase_agreement_lines
|
|
CREATE INDEX idx_purchase_agreement_lines_tenant_id ON purchase.purchase_agreement_lines(tenant_id);
|
|
|
|
-- RLS para purchase_agreement_lines
|
|
ALTER TABLE purchase.purchase_agreement_lines ENABLE ROW LEVEL SECURITY;
|
|
CREATE POLICY tenant_isolation_purchase_agreement_lines ON purchase.purchase_agreement_lines
|
|
USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid);
|
|
|
|
-- Tabla: vendor_evaluations (Evaluaciones de proveedores)
|
|
CREATE TABLE purchase.vendor_evaluations (
|
|
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,
|
|
|
|
partner_id UUID NOT NULL REFERENCES core.partners(id),
|
|
|
|
-- Período de evaluación
|
|
evaluation_date DATE NOT NULL,
|
|
period_start DATE NOT NULL,
|
|
period_end DATE NOT NULL,
|
|
|
|
-- Calificaciones (1-5)
|
|
quality_rating INTEGER,
|
|
delivery_rating INTEGER,
|
|
service_rating INTEGER,
|
|
price_rating INTEGER,
|
|
overall_rating DECIMAL(3, 2),
|
|
|
|
-- Métricas
|
|
on_time_delivery_rate DECIMAL(5, 2), -- Porcentaje
|
|
defect_rate DECIMAL(5, 2), -- Porcentaje
|
|
|
|
-- Comentarios
|
|
comments TEXT,
|
|
|
|
-- Auditoría
|
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
created_by UUID REFERENCES auth.users(id),
|
|
|
|
CONSTRAINT chk_vendor_evaluations_quality CHECK (quality_rating >= 1 AND quality_rating <= 5),
|
|
CONSTRAINT chk_vendor_evaluations_delivery CHECK (delivery_rating >= 1 AND delivery_rating <= 5),
|
|
CONSTRAINT chk_vendor_evaluations_service CHECK (service_rating >= 1 AND service_rating <= 5),
|
|
CONSTRAINT chk_vendor_evaluations_price CHECK (price_rating >= 1 AND price_rating <= 5),
|
|
CONSTRAINT chk_vendor_evaluations_overall CHECK (overall_rating >= 1 AND overall_rating <= 5),
|
|
CONSTRAINT chk_vendor_evaluations_dates CHECK (period_end >= period_start)
|
|
);
|
|
|
|
-- =====================================================
|
|
-- INDICES
|
|
-- =====================================================
|
|
|
|
-- Purchase Orders
|
|
CREATE INDEX idx_purchase_orders_tenant_id ON purchase.purchase_orders(tenant_id);
|
|
CREATE INDEX idx_purchase_orders_company_id ON purchase.purchase_orders(company_id);
|
|
CREATE INDEX idx_purchase_orders_partner_id ON purchase.purchase_orders(partner_id);
|
|
CREATE INDEX idx_purchase_orders_name ON purchase.purchase_orders(name);
|
|
CREATE INDEX idx_purchase_orders_status ON purchase.purchase_orders(status);
|
|
CREATE INDEX idx_purchase_orders_order_date ON purchase.purchase_orders(order_date);
|
|
CREATE INDEX idx_purchase_orders_expected_date ON purchase.purchase_orders(expected_date);
|
|
|
|
-- Purchase Order Lines
|
|
CREATE INDEX idx_purchase_order_lines_order_id ON purchase.purchase_order_lines(order_id);
|
|
CREATE INDEX idx_purchase_order_lines_product_id ON purchase.purchase_order_lines(product_id);
|
|
CREATE INDEX idx_purchase_order_lines_analytic_account_id ON purchase.purchase_order_lines(analytic_account_id) WHERE analytic_account_id IS NOT NULL;
|
|
|
|
-- RFQs
|
|
CREATE INDEX idx_rfqs_tenant_id ON purchase.rfqs(tenant_id);
|
|
CREATE INDEX idx_rfqs_company_id ON purchase.rfqs(company_id);
|
|
CREATE INDEX idx_rfqs_status ON purchase.rfqs(status);
|
|
CREATE INDEX idx_rfqs_request_date ON purchase.rfqs(request_date);
|
|
|
|
-- RFQ Lines
|
|
CREATE INDEX idx_rfq_lines_rfq_id ON purchase.rfq_lines(rfq_id);
|
|
CREATE INDEX idx_rfq_lines_product_id ON purchase.rfq_lines(product_id);
|
|
|
|
-- Vendor Pricelists
|
|
CREATE INDEX idx_vendor_pricelists_tenant_id ON purchase.vendor_pricelists(tenant_id);
|
|
CREATE INDEX idx_vendor_pricelists_partner_id ON purchase.vendor_pricelists(partner_id);
|
|
CREATE INDEX idx_vendor_pricelists_product_id ON purchase.vendor_pricelists(product_id);
|
|
CREATE INDEX idx_vendor_pricelists_active ON purchase.vendor_pricelists(active) WHERE active = TRUE;
|
|
|
|
-- Purchase Agreements
|
|
CREATE INDEX idx_purchase_agreements_tenant_id ON purchase.purchase_agreements(tenant_id);
|
|
CREATE INDEX idx_purchase_agreements_company_id ON purchase.purchase_agreements(company_id);
|
|
CREATE INDEX idx_purchase_agreements_partner_id ON purchase.purchase_agreements(partner_id);
|
|
CREATE INDEX idx_purchase_agreements_dates ON purchase.purchase_agreements(start_date, end_date);
|
|
CREATE INDEX idx_purchase_agreements_active ON purchase.purchase_agreements(is_active) WHERE is_active = TRUE;
|
|
|
|
-- Purchase Agreement Lines
|
|
CREATE INDEX idx_purchase_agreement_lines_agreement_id ON purchase.purchase_agreement_lines(agreement_id);
|
|
CREATE INDEX idx_purchase_agreement_lines_product_id ON purchase.purchase_agreement_lines(product_id);
|
|
|
|
-- Vendor Evaluations
|
|
CREATE INDEX idx_vendor_evaluations_tenant_id ON purchase.vendor_evaluations(tenant_id);
|
|
CREATE INDEX idx_vendor_evaluations_partner_id ON purchase.vendor_evaluations(partner_id);
|
|
CREATE INDEX idx_vendor_evaluations_date ON purchase.vendor_evaluations(evaluation_date);
|
|
|
|
-- =====================================================
|
|
-- FUNCTIONS
|
|
-- =====================================================
|
|
|
|
-- Función: calculate_purchase_order_totals
|
|
CREATE OR REPLACE FUNCTION purchase.calculate_purchase_order_totals(p_order_id UUID)
|
|
RETURNS VOID AS $$
|
|
DECLARE
|
|
v_amount_untaxed DECIMAL;
|
|
v_amount_tax DECIMAL;
|
|
v_amount_total DECIMAL;
|
|
BEGIN
|
|
SELECT
|
|
COALESCE(SUM(amount_untaxed), 0),
|
|
COALESCE(SUM(amount_tax), 0),
|
|
COALESCE(SUM(amount_total), 0)
|
|
INTO v_amount_untaxed, v_amount_tax, v_amount_total
|
|
FROM purchase.purchase_order_lines
|
|
WHERE order_id = p_order_id;
|
|
|
|
UPDATE purchase.purchase_orders
|
|
SET amount_untaxed = v_amount_untaxed,
|
|
amount_tax = v_amount_tax,
|
|
amount_total = v_amount_total,
|
|
updated_at = CURRENT_TIMESTAMP,
|
|
updated_by = get_current_user_id()
|
|
WHERE id = p_order_id;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
COMMENT ON FUNCTION purchase.calculate_purchase_order_totals IS 'Calcula los totales de una orden de compra';
|
|
|
|
-- Función: create_picking_from_po
|
|
CREATE OR REPLACE FUNCTION purchase.create_picking_from_po(p_order_id UUID)
|
|
RETURNS UUID AS $$
|
|
DECLARE
|
|
v_order RECORD;
|
|
v_picking_id UUID;
|
|
v_location_supplier UUID;
|
|
v_location_stock UUID;
|
|
BEGIN
|
|
-- Obtener datos de la orden
|
|
SELECT * INTO v_order
|
|
FROM purchase.purchase_orders
|
|
WHERE id = p_order_id;
|
|
|
|
IF NOT FOUND THEN
|
|
RAISE EXCEPTION 'Purchase order % not found', p_order_id;
|
|
END IF;
|
|
|
|
-- Obtener ubicaciones (simplificado - en producción obtener de configuración)
|
|
SELECT id INTO v_location_supplier
|
|
FROM inventory.locations
|
|
WHERE location_type = 'supplier'
|
|
LIMIT 1;
|
|
|
|
SELECT id INTO v_location_stock
|
|
FROM inventory.locations
|
|
WHERE location_type = 'internal'
|
|
LIMIT 1;
|
|
|
|
-- Crear picking
|
|
INSERT INTO inventory.pickings (
|
|
tenant_id,
|
|
company_id,
|
|
name,
|
|
picking_type,
|
|
location_id,
|
|
location_dest_id,
|
|
partner_id,
|
|
origin,
|
|
scheduled_date
|
|
) VALUES (
|
|
v_order.tenant_id,
|
|
v_order.company_id,
|
|
'IN/' || v_order.name,
|
|
'incoming',
|
|
v_location_supplier,
|
|
v_location_stock,
|
|
v_order.partner_id,
|
|
v_order.name,
|
|
v_order.expected_date
|
|
) RETURNING id INTO v_picking_id;
|
|
|
|
-- Actualizar la PO con el picking_id
|
|
UPDATE purchase.purchase_orders
|
|
SET picking_id = v_picking_id
|
|
WHERE id = p_order_id;
|
|
|
|
RETURN v_picking_id;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
COMMENT ON FUNCTION purchase.create_picking_from_po IS 'Crea un picking de recepción a partir de una orden de compra';
|
|
|
|
-- COR-009: Función de aprobación de órdenes de compra
|
|
CREATE OR REPLACE FUNCTION purchase.button_approve(p_order_id UUID)
|
|
RETURNS VOID AS $$
|
|
DECLARE
|
|
v_order RECORD;
|
|
BEGIN
|
|
-- Obtener datos de la orden
|
|
SELECT * INTO v_order
|
|
FROM purchase.purchase_orders
|
|
WHERE id = p_order_id;
|
|
|
|
IF NOT FOUND THEN
|
|
RAISE EXCEPTION 'Purchase order % not found', p_order_id;
|
|
END IF;
|
|
|
|
-- Verificar estado válido para aprobación
|
|
IF v_order.status != 'to_approve' THEN
|
|
RAISE EXCEPTION 'Purchase order % is not in to_approve status', p_order_id;
|
|
END IF;
|
|
|
|
-- Verificar que no esté bloqueada
|
|
IF v_order.locked THEN
|
|
RAISE EXCEPTION 'Purchase order % is locked', p_order_id;
|
|
END IF;
|
|
|
|
-- Aprobar la orden
|
|
UPDATE purchase.purchase_orders
|
|
SET status = 'purchase',
|
|
approved_at = CURRENT_TIMESTAMP,
|
|
approved_by = get_current_user_id(),
|
|
updated_at = CURRENT_TIMESTAMP,
|
|
updated_by = get_current_user_id()
|
|
WHERE id = p_order_id;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
COMMENT ON FUNCTION purchase.button_approve IS 'COR-009: Aprueba una orden de compra en estado to_approve (Odoo alignment)';
|
|
|
|
-- COR-009: Función para enviar a aprobación
|
|
CREATE OR REPLACE FUNCTION purchase.button_confirm(p_order_id UUID)
|
|
RETURNS VOID AS $$
|
|
DECLARE
|
|
v_order RECORD;
|
|
BEGIN
|
|
-- Obtener datos de la orden
|
|
SELECT * INTO v_order
|
|
FROM purchase.purchase_orders
|
|
WHERE id = p_order_id;
|
|
|
|
IF NOT FOUND THEN
|
|
RAISE EXCEPTION 'Purchase order % not found', p_order_id;
|
|
END IF;
|
|
|
|
-- Verificar estado válido
|
|
IF v_order.status NOT IN ('draft', 'sent') THEN
|
|
RAISE EXCEPTION 'Purchase order % cannot be confirmed from status %', p_order_id, v_order.status;
|
|
END IF;
|
|
|
|
-- Si requiere aprobación y supera threshold, enviar a aprobación
|
|
IF v_order.approval_required AND
|
|
v_order.amount_approval_threshold IS NOT NULL AND
|
|
v_order.amount_total > v_order.amount_approval_threshold THEN
|
|
UPDATE purchase.purchase_orders
|
|
SET status = 'to_approve',
|
|
updated_at = CURRENT_TIMESTAMP,
|
|
updated_by = get_current_user_id()
|
|
WHERE id = p_order_id;
|
|
ELSE
|
|
-- Confirmar directamente
|
|
UPDATE purchase.purchase_orders
|
|
SET status = 'purchase',
|
|
confirmed_at = CURRENT_TIMESTAMP,
|
|
confirmed_by = get_current_user_id(),
|
|
updated_at = CURRENT_TIMESTAMP,
|
|
updated_by = get_current_user_id()
|
|
WHERE id = p_order_id;
|
|
END IF;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
COMMENT ON FUNCTION purchase.button_confirm IS 'COR-009: Confirma una orden de compra, enviando a aprobación si supera threshold';
|
|
|
|
-- =====================================================
|
|
-- TRIGGERS
|
|
-- =====================================================
|
|
|
|
CREATE TRIGGER trg_purchase_orders_updated_at
|
|
BEFORE UPDATE ON purchase.purchase_orders
|
|
FOR EACH ROW
|
|
EXECUTE FUNCTION auth.update_updated_at_column();
|
|
|
|
CREATE TRIGGER trg_rfqs_updated_at
|
|
BEFORE UPDATE ON purchase.rfqs
|
|
FOR EACH ROW
|
|
EXECUTE FUNCTION auth.update_updated_at_column();
|
|
|
|
CREATE TRIGGER trg_vendor_pricelists_updated_at
|
|
BEFORE UPDATE ON purchase.vendor_pricelists
|
|
FOR EACH ROW
|
|
EXECUTE FUNCTION auth.update_updated_at_column();
|
|
|
|
CREATE TRIGGER trg_purchase_agreements_updated_at
|
|
BEFORE UPDATE ON purchase.purchase_agreements
|
|
FOR EACH ROW
|
|
EXECUTE FUNCTION auth.update_updated_at_column();
|
|
|
|
-- Trigger: Actualizar totales de PO al cambiar líneas
|
|
CREATE OR REPLACE FUNCTION purchase.trg_update_po_totals()
|
|
RETURNS TRIGGER AS $$
|
|
BEGIN
|
|
IF TG_OP = 'DELETE' THEN
|
|
PERFORM purchase.calculate_purchase_order_totals(OLD.order_id);
|
|
ELSE
|
|
PERFORM purchase.calculate_purchase_order_totals(NEW.order_id);
|
|
END IF;
|
|
RETURN NULL;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
CREATE TRIGGER trg_purchase_order_lines_update_totals
|
|
AFTER INSERT OR UPDATE OR DELETE ON purchase.purchase_order_lines
|
|
FOR EACH ROW
|
|
EXECUTE FUNCTION purchase.trg_update_po_totals();
|
|
|
|
-- =====================================================
|
|
-- TRACKING AUTOMÁTICO (mail.thread pattern)
|
|
-- =====================================================
|
|
|
|
-- Trigger: Tracking automático para órdenes de compra
|
|
CREATE TRIGGER track_purchase_order_changes
|
|
AFTER INSERT OR UPDATE OR DELETE ON purchase.purchase_orders
|
|
FOR EACH ROW EXECUTE FUNCTION system.track_field_changes();
|
|
|
|
COMMENT ON TRIGGER track_purchase_order_changes ON purchase.purchase_orders IS
|
|
'Registra automáticamente cambios en órdenes de compra (estado, proveedor, monto, fecha)';
|
|
|
|
-- =====================================================
|
|
-- ROW LEVEL SECURITY (RLS)
|
|
-- =====================================================
|
|
|
|
ALTER TABLE purchase.purchase_orders ENABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE purchase.rfqs ENABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE purchase.vendor_pricelists ENABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE purchase.purchase_agreements ENABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE purchase.vendor_evaluations ENABLE ROW LEVEL SECURITY;
|
|
|
|
CREATE POLICY tenant_isolation_purchase_orders ON purchase.purchase_orders
|
|
USING (tenant_id = get_current_tenant_id());
|
|
|
|
CREATE POLICY tenant_isolation_rfqs ON purchase.rfqs
|
|
USING (tenant_id = get_current_tenant_id());
|
|
|
|
CREATE POLICY tenant_isolation_vendor_pricelists ON purchase.vendor_pricelists
|
|
USING (tenant_id = get_current_tenant_id());
|
|
|
|
CREATE POLICY tenant_isolation_purchase_agreements ON purchase.purchase_agreements
|
|
USING (tenant_id = get_current_tenant_id());
|
|
|
|
CREATE POLICY tenant_isolation_vendor_evaluations ON purchase.vendor_evaluations
|
|
USING (tenant_id = get_current_tenant_id());
|
|
|
|
-- =====================================================
|
|
-- COMENTARIOS
|
|
-- =====================================================
|
|
|
|
COMMENT ON SCHEMA purchase IS 'Schema de gestión de compras y proveedores';
|
|
COMMENT ON TABLE purchase.purchase_orders IS 'Órdenes de compra a proveedores';
|
|
COMMENT ON TABLE purchase.purchase_order_lines IS 'Líneas de órdenes de compra';
|
|
COMMENT ON TABLE purchase.rfqs IS 'Solicitudes de cotización (RFQ)';
|
|
COMMENT ON TABLE purchase.rfq_lines IS 'Líneas de solicitudes de cotización';
|
|
COMMENT ON TABLE purchase.vendor_pricelists IS 'Listas de precios de proveedores';
|
|
COMMENT ON TABLE purchase.purchase_agreements IS 'Acuerdos/contratos de compra con proveedores';
|
|
COMMENT ON TABLE purchase.purchase_agreement_lines IS 'Líneas de acuerdos de compra';
|
|
COMMENT ON TABLE purchase.vendor_evaluations IS 'Evaluaciones de desempeño de proveedores';
|
|
|
|
-- =====================================================
|
|
-- COR-029: Purchase Order Functions
|
|
-- Funciones de cancel y draft para PO
|
|
-- =====================================================
|
|
|
|
-- Funcion: button_cancel
|
|
CREATE OR REPLACE FUNCTION purchase.button_cancel(p_order_id UUID)
|
|
RETURNS VOID AS $$
|
|
DECLARE
|
|
v_order RECORD;
|
|
BEGIN
|
|
SELECT * INTO v_order FROM purchase.purchase_orders WHERE id = p_order_id;
|
|
|
|
IF NOT FOUND THEN
|
|
RAISE EXCEPTION 'Purchase order % not found', p_order_id;
|
|
END IF;
|
|
|
|
IF v_order.locked THEN
|
|
RAISE EXCEPTION 'Cannot cancel locked order';
|
|
END IF;
|
|
|
|
IF v_order.status = 'cancelled' THEN
|
|
RETURN;
|
|
END IF;
|
|
|
|
-- Cancel related pickings
|
|
UPDATE inventory.pickings
|
|
SET status = 'cancelled'
|
|
WHERE origin_document_type = 'purchase_order'
|
|
AND origin_document_id = p_order_id
|
|
AND status != 'done';
|
|
|
|
-- Update order status
|
|
UPDATE purchase.purchase_orders
|
|
SET status = 'cancelled', updated_at = NOW()
|
|
WHERE id = p_order_id;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
-- Funcion: button_draft
|
|
CREATE OR REPLACE FUNCTION purchase.button_draft(p_order_id UUID)
|
|
RETURNS VOID AS $$
|
|
DECLARE
|
|
v_order RECORD;
|
|
BEGIN
|
|
SELECT * INTO v_order FROM purchase.purchase_orders WHERE id = p_order_id;
|
|
|
|
IF NOT FOUND THEN
|
|
RAISE EXCEPTION 'Purchase order % not found', p_order_id;
|
|
END IF;
|
|
|
|
IF v_order.status NOT IN ('cancelled', 'sent') THEN
|
|
RAISE EXCEPTION 'Can only set to draft from cancelled or sent state';
|
|
END IF;
|
|
|
|
UPDATE purchase.purchase_orders
|
|
SET status = 'draft', updated_at = NOW()
|
|
WHERE id = p_order_id;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
COMMENT ON FUNCTION purchase.button_cancel IS 'COR-029: Cancel purchase order and related pickings';
|
|
COMMENT ON FUNCTION purchase.button_draft IS 'COR-029: Set purchase order back to draft state';
|
|
|
|
-- =====================================================
|
|
-- COR-045: Product Supplierinfo
|
|
-- Equivalente a product.supplierinfo de Odoo
|
|
-- =====================================================
|
|
|
|
CREATE TABLE purchase.product_supplierinfo (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE,
|
|
company_id UUID REFERENCES auth.companies(id) ON DELETE CASCADE,
|
|
|
|
-- Producto y proveedor
|
|
product_id UUID REFERENCES inventory.products(id) ON DELETE CASCADE,
|
|
product_tmpl_id UUID, -- Para future product templates
|
|
partner_id UUID NOT NULL REFERENCES core.partners(id),
|
|
|
|
-- Referencia del proveedor
|
|
product_name VARCHAR(255), -- Nombre del producto en catalogo proveedor
|
|
product_code VARCHAR(100), -- Codigo del proveedor
|
|
|
|
-- Precios
|
|
price DECIMAL(20,6) NOT NULL DEFAULT 0,
|
|
currency_id UUID REFERENCES core.currencies(id),
|
|
|
|
-- Cantidades
|
|
min_qty DECIMAL(20,6) DEFAULT 0, -- Cantidad minima
|
|
|
|
-- Tiempos de entrega
|
|
delay INTEGER DEFAULT 1, -- Dias de entrega
|
|
|
|
-- Validez
|
|
date_start DATE,
|
|
date_end DATE,
|
|
|
|
-- Secuencia para ordenar proveedores
|
|
sequence INTEGER DEFAULT 1,
|
|
|
|
-- Control
|
|
is_active BOOLEAN DEFAULT TRUE,
|
|
|
|
-- Auditoria
|
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
created_by UUID REFERENCES auth.users(id),
|
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
|
|
CREATE INDEX idx_product_supplierinfo_tenant ON purchase.product_supplierinfo(tenant_id);
|
|
CREATE INDEX idx_product_supplierinfo_product ON purchase.product_supplierinfo(product_id);
|
|
CREATE INDEX idx_product_supplierinfo_partner ON purchase.product_supplierinfo(partner_id);
|
|
CREATE INDEX idx_product_supplierinfo_sequence ON purchase.product_supplierinfo(sequence);
|
|
|
|
-- RLS
|
|
ALTER TABLE purchase.product_supplierinfo ENABLE ROW LEVEL SECURITY;
|
|
CREATE POLICY tenant_isolation_product_supplierinfo ON purchase.product_supplierinfo
|
|
USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid);
|
|
|
|
COMMENT ON TABLE purchase.product_supplierinfo IS 'COR-045: Product supplier info - Equivalent to product.supplierinfo';
|
|
|
|
-- =====================================================
|
|
-- COR-046: Purchase Order Additional Fields
|
|
-- Campos adicionales para alinear con Odoo
|
|
-- =====================================================
|
|
|
|
-- Agregar campos a purchase_orders
|
|
ALTER TABLE purchase.purchase_orders
|
|
ADD COLUMN IF NOT EXISTS user_id UUID REFERENCES auth.users(id),
|
|
ADD COLUMN IF NOT EXISTS incoterm_id UUID, -- FK to financial.incoterms
|
|
ADD COLUMN IF NOT EXISTS incoterm_location VARCHAR(255),
|
|
ADD COLUMN IF NOT EXISTS fiscal_position_id UUID, -- FK to financial.fiscal_positions
|
|
ADD COLUMN IF NOT EXISTS origin VARCHAR(255), -- Documento origen
|
|
ADD COLUMN IF NOT EXISTS date_planned TIMESTAMP WITH TIME ZONE, -- Fecha esperada receipt
|
|
ADD COLUMN IF NOT EXISTS date_approve TIMESTAMP WITH TIME ZONE; -- Fecha de aprobacion
|
|
|
|
-- Agregar campos a purchase_order_lines
|
|
ALTER TABLE purchase.purchase_order_lines
|
|
ADD COLUMN IF NOT EXISTS sequence INTEGER DEFAULT 10,
|
|
ADD COLUMN IF NOT EXISTS product_packaging_id UUID, -- FK future packaging table
|
|
ADD COLUMN IF NOT EXISTS product_packaging_qty DECIMAL(20,6),
|
|
ADD COLUMN IF NOT EXISTS qty_to_receive DECIMAL(20,6) GENERATED ALWAYS AS (quantity - qty_received) STORED,
|
|
ADD COLUMN IF NOT EXISTS price_subtotal DECIMAL(20,6), -- Computed subtotal
|
|
ADD COLUMN IF NOT EXISTS date_planned TIMESTAMP WITH TIME ZONE;
|
|
|
|
CREATE INDEX idx_purchase_orders_user ON purchase.purchase_orders(user_id);
|
|
CREATE INDEX idx_purchase_orders_origin ON purchase.purchase_orders(origin);
|
|
|
|
COMMENT ON COLUMN purchase.purchase_orders.incoterm_id IS 'COR-046: Incoterm reference';
|
|
COMMENT ON COLUMN purchase.purchase_orders.origin IS 'COR-046: Source document reference';
|
|
|
|
-- =====================================================
|
|
-- COR-047: Purchase Order Confirm with Stock Move
|
|
-- Funcion para confirmar PO y crear stock moves
|
|
-- =====================================================
|
|
|
|
CREATE OR REPLACE FUNCTION purchase.action_create_stock_moves(p_order_id UUID)
|
|
RETURNS UUID AS $$
|
|
DECLARE
|
|
v_order RECORD;
|
|
v_line RECORD;
|
|
v_picking_id UUID;
|
|
v_move_id UUID;
|
|
v_location_supplier UUID;
|
|
v_location_dest UUID;
|
|
v_picking_type_id UUID;
|
|
BEGIN
|
|
-- Obtener orden
|
|
SELECT * INTO v_order FROM purchase.purchase_orders WHERE id = p_order_id;
|
|
|
|
IF NOT FOUND THEN
|
|
RAISE EXCEPTION 'Purchase order % not found', p_order_id;
|
|
END IF;
|
|
|
|
-- Obtener ubicaciones
|
|
SELECT id INTO v_location_supplier
|
|
FROM inventory.locations
|
|
WHERE location_type = 'supplier' AND tenant_id = v_order.tenant_id
|
|
LIMIT 1;
|
|
|
|
SELECT id INTO v_location_dest
|
|
FROM inventory.locations
|
|
WHERE location_type = 'internal' AND tenant_id = v_order.tenant_id
|
|
LIMIT 1;
|
|
|
|
-- Obtener picking type de recepcion
|
|
SELECT id INTO v_picking_type_id
|
|
FROM inventory.picking_types
|
|
WHERE code = 'incoming' AND tenant_id = v_order.tenant_id
|
|
LIMIT 1;
|
|
|
|
-- Crear picking si no existe
|
|
IF v_order.picking_id IS NULL THEN
|
|
INSERT INTO inventory.pickings (
|
|
tenant_id, company_id, name, picking_type,
|
|
location_id, location_dest_id, partner_id,
|
|
origin, scheduled_date, status
|
|
) VALUES (
|
|
v_order.tenant_id, v_order.company_id,
|
|
'IN/' || v_order.name,
|
|
'incoming',
|
|
v_location_supplier, v_location_dest,
|
|
v_order.partner_id,
|
|
v_order.name,
|
|
v_order.expected_date,
|
|
'draft'
|
|
) RETURNING id INTO v_picking_id;
|
|
|
|
UPDATE purchase.purchase_orders SET picking_id = v_picking_id WHERE id = p_order_id;
|
|
ELSE
|
|
v_picking_id := v_order.picking_id;
|
|
END IF;
|
|
|
|
-- Crear stock moves para cada linea
|
|
FOR v_line IN
|
|
SELECT * FROM purchase.purchase_order_lines WHERE order_id = p_order_id
|
|
LOOP
|
|
INSERT INTO inventory.stock_moves (
|
|
tenant_id, picking_id, product_id,
|
|
product_uom_id, product_qty,
|
|
location_id, location_dest_id,
|
|
origin, state, name
|
|
) VALUES (
|
|
v_order.tenant_id, v_picking_id, v_line.product_id,
|
|
v_line.uom_id, v_line.quantity,
|
|
v_location_supplier, v_location_dest,
|
|
v_order.name, 'draft', v_line.description
|
|
) RETURNING id INTO v_move_id;
|
|
END LOOP;
|
|
|
|
RETURN v_picking_id;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
COMMENT ON FUNCTION purchase.action_create_stock_moves IS 'COR-047: Create stock moves from confirmed PO';
|
|
|
|
-- =====================================================
|
|
-- FIN DEL SCHEMA PURCHASE
|
|
-- =====================================================
|