-- =========================================== -- MECANICAS DIESEL - Schema de Compras -- =========================================== -- Sistema de ordenes de compra, proveedores y recepciones -- Gestion completa del ciclo de compras del taller -- ============================================ -- SCHEMA: purchasing -- ============================================ CREATE SCHEMA IF NOT EXISTS purchasing; COMMENT ON SCHEMA purchasing IS 'Gestion de compras: ordenes de compra, recepciones, proveedores'; -- Grants GRANT USAGE ON SCHEMA purchasing TO mecanicas_user; GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA purchasing TO mecanicas_user; ALTER DEFAULT PRIVILEGES IN SCHEMA purchasing GRANT ALL ON TABLES TO mecanicas_user; -- ============================================ -- PROVEEDORES (complementa partners existentes) -- ============================================ -- Extension de datos de proveedor CREATE TABLE purchasing.suppliers ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL, -- Datos basicos code VARCHAR(20) NOT NULL, name VARCHAR(256) NOT NULL, trade_name VARCHAR(256), -- Nombre comercial rfc VARCHAR(13), -- Contacto contact_name VARCHAR(256), email VARCHAR(256), phone VARCHAR(50), mobile VARCHAR(50), -- Direccion street VARCHAR(256), city VARCHAR(100), state VARCHAR(100), zip_code VARCHAR(10), country VARCHAR(100) DEFAULT 'Mexico', -- Datos comerciales payment_term_days INTEGER DEFAULT 30, credit_limit DECIMAL(20,6) DEFAULT 0, currency_code VARCHAR(3) DEFAULT 'MXN', -- Clasificacion category VARCHAR(50), -- refacciones, lubricantes, herramientas, etc. is_preferred BOOLEAN NOT NULL DEFAULT false, rating INTEGER CHECK (rating BETWEEN 1 AND 5), -- Notas notes TEXT, -- Metadatos is_active BOOLEAN NOT NULL DEFAULT true, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ, created_by UUID, CONSTRAINT uq_supplier_code UNIQUE(tenant_id, code) ); COMMENT ON TABLE purchasing.suppliers IS 'Proveedores del taller'; -- Indices para suppliers CREATE INDEX idx_suppliers_tenant ON purchasing.suppliers(tenant_id); CREATE INDEX idx_suppliers_name ON purchasing.suppliers(name); CREATE INDEX idx_suppliers_category ON purchasing.suppliers(category); CREATE INDEX idx_suppliers_preferred ON purchasing.suppliers(is_preferred) WHERE is_preferred = true; -- RLS para suppliers SELECT create_tenant_rls_policies('purchasing', 'suppliers'); -- Trigger para updated_at CREATE TRIGGER set_updated_at_suppliers BEFORE UPDATE ON purchasing.suppliers FOR EACH ROW EXECUTE FUNCTION trigger_set_updated_at(); -- ============================================ -- ORDENES DE COMPRA -- ============================================ -- Estados de orden de compra CREATE TYPE purchasing.po_status AS ENUM ( 'draft', -- Borrador 'sent', -- Enviada a proveedor 'confirmed', -- Confirmada por proveedor 'partial', -- Parcialmente recibida 'received', -- Completamente recibida 'cancelled' -- Cancelada ); -- Ordenes de compra CREATE TABLE purchasing.purchase_orders ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL, -- Identificacion order_number VARCHAR(50) NOT NULL, reference VARCHAR(100), -- Referencia del proveedor -- Proveedor supplier_id UUID NOT NULL REFERENCES purchasing.suppliers(id), -- Estado status purchasing.po_status NOT NULL DEFAULT 'draft', -- Fechas order_date DATE NOT NULL DEFAULT CURRENT_DATE, expected_date DATE, -- Fecha esperada de entrega received_date DATE, -- Fecha de recepcion completa -- Importes subtotal DECIMAL(20,6) NOT NULL DEFAULT 0, discount_amount DECIMAL(20,6) NOT NULL DEFAULT 0, tax_amount DECIMAL(20,6) NOT NULL DEFAULT 0, total DECIMAL(20,6) NOT NULL DEFAULT 0, currency_code VARCHAR(3) DEFAULT 'MXN', -- Urgencia (para taller) priority VARCHAR(20) DEFAULT 'normal', service_order_id UUID, -- Orden de servicio relacionada (si aplica) -- Notas notes TEXT, internal_notes TEXT, -- Auditoria created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ, created_by UUID NOT NULL, confirmed_by UUID, confirmed_at TIMESTAMPTZ, CONSTRAINT uq_po_number UNIQUE(tenant_id, order_number), CONSTRAINT chk_po_priority CHECK (priority IN ('low', 'normal', 'high', 'urgent')) ); COMMENT ON TABLE purchasing.purchase_orders IS 'Ordenes de compra a proveedores'; COMMENT ON COLUMN purchasing.purchase_orders.service_order_id IS 'Orden de servicio que origino la compra (para urgencias)'; -- Indices para purchase_orders CREATE INDEX idx_po_tenant ON purchasing.purchase_orders(tenant_id); CREATE INDEX idx_po_supplier ON purchasing.purchase_orders(supplier_id); CREATE INDEX idx_po_status ON purchasing.purchase_orders(status); CREATE INDEX idx_po_date ON purchasing.purchase_orders(order_date DESC); CREATE INDEX idx_po_expected ON purchasing.purchase_orders(expected_date) WHERE status NOT IN ('received', 'cancelled'); CREATE INDEX idx_po_service_order ON purchasing.purchase_orders(service_order_id) WHERE service_order_id IS NOT NULL; -- RLS para purchase_orders SELECT create_tenant_rls_policies('purchasing', 'purchase_orders'); -- Triggers CREATE TRIGGER set_updated_at_purchase_orders BEFORE UPDATE ON purchasing.purchase_orders FOR EACH ROW EXECUTE FUNCTION trigger_set_updated_at(); -- ============================================ -- LINEAS DE ORDEN DE COMPRA -- ============================================ CREATE TABLE purchasing.purchase_order_lines ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), purchase_order_id UUID NOT NULL REFERENCES purchasing.purchase_orders(id) ON DELETE CASCADE, -- Linea line_number INTEGER NOT NULL DEFAULT 1, -- Producto part_id UUID, -- Referencia a parts_management.parts product_code VARCHAR(50), -- Codigo del producto (desnormalizado) description VARCHAR(500) NOT NULL, -- Cantidades quantity DECIMAL(20,6) NOT NULL, unit_of_measure VARCHAR(20) DEFAULT 'PZA', received_quantity DECIMAL(20,6) NOT NULL DEFAULT 0, -- Precios unit_price DECIMAL(20,6) NOT NULL, discount_percent DECIMAL(5,2) NOT NULL DEFAULT 0, subtotal DECIMAL(20,6) NOT NULL, tax_percent DECIMAL(5,2) NOT NULL DEFAULT 16.00, -- IVA Mexico tax_amount DECIMAL(20,6) NOT NULL DEFAULT 0, total DECIMAL(20,6) NOT NULL, -- Fechas expected_date DATE, -- Estado de linea is_closed BOOLEAN NOT NULL DEFAULT false, -- Auditoria created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); COMMENT ON TABLE purchasing.purchase_order_lines IS 'Lineas de detalle de ordenes de compra'; -- Indices para lines CREATE INDEX idx_pol_order ON purchasing.purchase_order_lines(purchase_order_id); CREATE INDEX idx_pol_part ON purchasing.purchase_order_lines(part_id) WHERE part_id IS NOT NULL; CREATE INDEX idx_pol_pending ON purchasing.purchase_order_lines(purchase_order_id) WHERE received_quantity < quantity AND is_closed = false; -- ============================================ -- RECEPCIONES DE COMPRA -- ============================================ CREATE TABLE purchasing.purchase_receipts ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL, -- Identificacion receipt_number VARCHAR(50) NOT NULL, purchase_order_id UUID NOT NULL REFERENCES purchasing.purchase_orders(id), -- Fecha y proveedor receipt_date DATE NOT NULL DEFAULT CURRENT_DATE, supplier_id UUID NOT NULL REFERENCES purchasing.suppliers(id), -- Documentos del proveedor supplier_invoice VARCHAR(50), -- Numero de factura proveedor supplier_delivery_note VARCHAR(50), -- Remision del proveedor -- Notas notes TEXT, -- Auditoria created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), created_by UUID NOT NULL, CONSTRAINT uq_receipt_number UNIQUE(tenant_id, receipt_number) ); COMMENT ON TABLE purchasing.purchase_receipts IS 'Recepciones de mercancia de ordenes de compra'; -- Indices para receipts CREATE INDEX idx_pr_tenant ON purchasing.purchase_receipts(tenant_id); CREATE INDEX idx_pr_order ON purchasing.purchase_receipts(purchase_order_id); CREATE INDEX idx_pr_date ON purchasing.purchase_receipts(receipt_date DESC); -- RLS para receipts SELECT create_tenant_rls_policies('purchasing', 'purchase_receipts'); -- Lineas de recepcion CREATE TABLE purchasing.purchase_receipt_lines ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), receipt_id UUID NOT NULL REFERENCES purchasing.purchase_receipts(id) ON DELETE CASCADE, order_line_id UUID NOT NULL REFERENCES purchasing.purchase_order_lines(id), -- Cantidades quantity_received DECIMAL(20,6) NOT NULL, quantity_rejected DECIMAL(20,6) NOT NULL DEFAULT 0, rejection_reason VARCHAR(256), -- Lote/Serie (si aplica) lot_number VARCHAR(100), serial_numbers TEXT[], -- Ubicacion destino location_id UUID, -- Referencia a inventory location -- Auditoria created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); COMMENT ON TABLE purchasing.purchase_receipt_lines IS 'Detalle de productos recibidos'; CREATE INDEX idx_prl_receipt ON purchasing.purchase_receipt_lines(receipt_id); CREATE INDEX idx_prl_order_line ON purchasing.purchase_receipt_lines(order_line_id); -- ============================================ -- FUNCIONES AUXILIARES -- ============================================ -- Funcion para generar numero de orden de compra CREATE OR REPLACE FUNCTION purchasing.generate_po_number(p_tenant_id UUID) RETURNS VARCHAR(50) AS $$ DECLARE v_year TEXT; v_sequence INTEGER; v_number VARCHAR(50); BEGIN v_year := TO_CHAR(CURRENT_DATE, 'YYYY'); -- Obtener siguiente secuencia del año SELECT COALESCE(MAX( CAST(SUBSTRING(order_number FROM 'OC-' || v_year || '-(\d+)') AS INTEGER) ), 0) + 1 INTO v_sequence FROM purchasing.purchase_orders WHERE tenant_id = p_tenant_id AND order_number LIKE 'OC-' || v_year || '-%'; v_number := 'OC-' || v_year || '-' || LPAD(v_sequence::TEXT, 5, '0'); RETURN v_number; END; $$ LANGUAGE plpgsql; COMMENT ON FUNCTION purchasing.generate_po_number IS 'Genera numero secuencial de orden de compra (OC-YYYY-NNNNN)'; -- Funcion para generar numero de recepcion CREATE OR REPLACE FUNCTION purchasing.generate_receipt_number(p_tenant_id UUID) RETURNS VARCHAR(50) AS $$ DECLARE v_year TEXT; v_sequence INTEGER; v_number VARCHAR(50); BEGIN v_year := TO_CHAR(CURRENT_DATE, 'YYYY'); SELECT COALESCE(MAX( CAST(SUBSTRING(receipt_number FROM 'REC-' || v_year || '-(\d+)') AS INTEGER) ), 0) + 1 INTO v_sequence FROM purchasing.purchase_receipts WHERE tenant_id = p_tenant_id AND receipt_number LIKE 'REC-' || v_year || '-%'; v_number := 'REC-' || v_year || '-' || LPAD(v_sequence::TEXT, 5, '0'); RETURN v_number; END; $$ LANGUAGE plpgsql; COMMENT ON FUNCTION purchasing.generate_receipt_number IS 'Genera numero secuencial de recepcion (REC-YYYY-NNNNN)'; -- Funcion para calcular totales de linea CREATE OR REPLACE FUNCTION purchasing.calculate_line_totals() RETURNS TRIGGER AS $$ BEGIN -- Calcular subtotal (con descuento) NEW.subtotal := NEW.quantity * NEW.unit_price * (1 - NEW.discount_percent / 100); -- Calcular impuesto NEW.tax_amount := NEW.subtotal * NEW.tax_percent / 100; -- Calcular total NEW.total := NEW.subtotal + NEW.tax_amount; RETURN NEW; END; $$ LANGUAGE plpgsql; CREATE TRIGGER calculate_pol_totals BEFORE INSERT OR UPDATE OF quantity, unit_price, discount_percent, tax_percent ON purchasing.purchase_order_lines FOR EACH ROW EXECUTE FUNCTION purchasing.calculate_line_totals(); -- Funcion para actualizar totales de orden CREATE OR REPLACE FUNCTION purchasing.update_order_totals() RETURNS TRIGGER AS $$ BEGIN UPDATE purchasing.purchase_orders po SET subtotal = COALESCE(( SELECT SUM(subtotal) FROM purchasing.purchase_order_lines WHERE purchase_order_id = po.id ), 0), tax_amount = COALESCE(( SELECT SUM(tax_amount) FROM purchasing.purchase_order_lines WHERE purchase_order_id = po.id ), 0), total = COALESCE(( SELECT SUM(total) FROM purchasing.purchase_order_lines WHERE purchase_order_id = po.id ), 0), updated_at = NOW() WHERE id = COALESCE(NEW.purchase_order_id, OLD.purchase_order_id); RETURN NULL; END; $$ LANGUAGE plpgsql; CREATE TRIGGER update_po_totals AFTER INSERT OR UPDATE OR DELETE ON purchasing.purchase_order_lines FOR EACH ROW EXECUTE FUNCTION purchasing.update_order_totals(); -- Funcion para actualizar cantidades recibidas CREATE OR REPLACE FUNCTION purchasing.update_received_quantities() RETURNS TRIGGER AS $$ DECLARE v_order_id UUID; v_total_lines INTEGER; v_received_lines INTEGER; BEGIN -- Actualizar cantidad recibida en linea de orden UPDATE purchasing.purchase_order_lines pol SET received_quantity = COALESCE(( SELECT SUM(prl.quantity_received) FROM purchasing.purchase_receipt_lines prl WHERE prl.order_line_id = pol.id ), 0) WHERE id = NEW.order_line_id; -- Obtener orden de compra SELECT purchase_order_id INTO v_order_id FROM purchasing.purchase_order_lines WHERE id = NEW.order_line_id; -- Verificar si toda la orden fue recibida SELECT COUNT(*), COUNT(*) FILTER (WHERE received_quantity >= quantity) INTO v_total_lines, v_received_lines FROM purchasing.purchase_order_lines WHERE purchase_order_id = v_order_id; -- Actualizar estado de la orden UPDATE purchasing.purchase_orders SET status = CASE WHEN v_received_lines = v_total_lines THEN 'received'::purchasing.po_status WHEN v_received_lines > 0 THEN 'partial'::purchasing.po_status ELSE status END, received_date = CASE WHEN v_received_lines = v_total_lines THEN CURRENT_DATE ELSE received_date END, updated_at = NOW() WHERE id = v_order_id; RETURN NULL; END; $$ LANGUAGE plpgsql; CREATE TRIGGER update_po_received AFTER INSERT ON purchasing.purchase_receipt_lines FOR EACH ROW EXECUTE FUNCTION purchasing.update_received_quantities(); -- ============================================ -- VISTAS UTILES -- ============================================ -- Vista de ordenes de compra pendientes CREATE VIEW purchasing.v_pending_orders AS SELECT po.id, po.tenant_id, po.order_number, po.status, po.order_date, po.expected_date, s.name as supplier_name, s.contact_name, s.phone as supplier_phone, po.total, po.priority, po.service_order_id, CASE WHEN po.expected_date < CURRENT_DATE THEN 'overdue' WHEN po.expected_date = CURRENT_DATE THEN 'today' WHEN po.expected_date <= CURRENT_DATE + 3 THEN 'soon' ELSE 'normal' END as urgency, COUNT(pol.id) as line_count, SUM(CASE WHEN pol.received_quantity < pol.quantity THEN 1 ELSE 0 END) as pending_lines FROM purchasing.purchase_orders po JOIN purchasing.suppliers s ON s.id = po.supplier_id LEFT JOIN purchasing.purchase_order_lines pol ON pol.purchase_order_id = po.id WHERE po.status NOT IN ('received', 'cancelled') GROUP BY po.id, po.tenant_id, po.order_number, po.status, po.order_date, po.expected_date, s.name, s.contact_name, s.phone, po.total, po.priority, po.service_order_id ORDER BY po.expected_date ASC NULLS LAST, po.priority DESC; COMMENT ON VIEW purchasing.v_pending_orders IS 'Ordenes de compra pendientes de recibir'; -- Vista de productos pendientes de recibir CREATE VIEW purchasing.v_pending_products AS SELECT po.tenant_id, pol.part_id, pol.product_code, pol.description, po.order_number, po.supplier_id, s.name as supplier_name, pol.quantity, pol.received_quantity, pol.quantity - pol.received_quantity as pending_quantity, pol.unit_price, po.expected_date, po.service_order_id FROM purchasing.purchase_order_lines pol JOIN purchasing.purchase_orders po ON po.id = pol.purchase_order_id JOIN purchasing.suppliers s ON s.id = po.supplier_id WHERE pol.received_quantity < pol.quantity AND pol.is_closed = false AND po.status NOT IN ('cancelled') ORDER BY po.expected_date ASC NULLS LAST; COMMENT ON VIEW purchasing.v_pending_products IS 'Productos pendientes de recibir por orden'; -- Vista de historial de compras por proveedor CREATE VIEW purchasing.v_supplier_history AS SELECT s.id as supplier_id, s.tenant_id, s.code as supplier_code, s.name as supplier_name, s.category, s.rating, COUNT(DISTINCT po.id) as total_orders, COUNT(DISTINCT po.id) FILTER (WHERE po.status = 'received') as completed_orders, SUM(po.total) FILTER (WHERE po.status = 'received') as total_purchased, AVG(po.received_date - po.expected_date) FILTER (WHERE po.status = 'received' AND po.expected_date IS NOT NULL) as avg_delivery_days, MAX(po.order_date) as last_order_date FROM purchasing.suppliers s LEFT JOIN purchasing.purchase_orders po ON po.supplier_id = s.id WHERE s.is_active = true GROUP BY s.id, s.tenant_id, s.code, s.name, s.category, s.rating ORDER BY total_purchased DESC NULLS LAST; COMMENT ON VIEW purchasing.v_supplier_history IS 'Historial y estadisticas de compras por proveedor'; -- ============================================ -- GRANTS ADICIONALES -- ============================================ GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA purchasing TO mecanicas_user; GRANT SELECT ON ALL TABLES IN SCHEMA purchasing TO mecanicas_user;