-- ===================================================== -- SCHEMA: core -- PROPÓSITO: Catálogos maestros y entidades fundamentales -- MÓDULOS: MGN-002 (Empresas), MGN-003 (Catálogos Maestros) -- FECHA: 2025-11-24 -- ===================================================== -- Crear schema CREATE SCHEMA IF NOT EXISTS core; -- ===================================================== -- TYPES (ENUMs) -- ===================================================== CREATE TYPE core.partner_type AS ENUM ( 'person', 'company' ); CREATE TYPE core.partner_category AS ENUM ( 'customer', 'supplier', 'employee', 'contact', 'other' ); CREATE TYPE core.address_type AS ENUM ( 'billing', 'shipping', 'contact', 'other' ); CREATE TYPE core.uom_type AS ENUM ( 'reference', 'bigger', 'smaller' ); -- ===================================================== -- TABLES -- ===================================================== -- Tabla: countries (Países - ISO 3166-1) CREATE TABLE core.countries ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), code VARCHAR(2) NOT NULL UNIQUE, -- ISO 3166-1 alpha-2 name VARCHAR(255) NOT NULL, phone_code VARCHAR(10), currency_code VARCHAR(3), -- ISO 4217 -- Sin tenant_id: catálogo global created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ); -- Tabla: currencies (Monedas - ISO 4217) CREATE TABLE core.currencies ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), code VARCHAR(3) NOT NULL UNIQUE, -- ISO 4217 name VARCHAR(100) NOT NULL, symbol VARCHAR(10) NOT NULL, decimals INTEGER NOT NULL DEFAULT 2, rounding DECIMAL(12, 6) DEFAULT 0.01, active BOOLEAN NOT NULL DEFAULT TRUE, -- Sin tenant_id: catálogo global created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ); -- Tabla: exchange_rates (Tasas de cambio) CREATE TABLE core.exchange_rates ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), from_currency_id UUID NOT NULL REFERENCES core.currencies(id), to_currency_id UUID NOT NULL REFERENCES core.currencies(id), rate DECIMAL(12, 6) NOT NULL, date DATE NOT NULL, -- Sin tenant_id: catálogo global created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, CONSTRAINT uq_exchange_rates_currencies_date UNIQUE (from_currency_id, to_currency_id, date), CONSTRAINT chk_exchange_rates_rate CHECK (rate > 0), CONSTRAINT chk_exchange_rates_different_currencies CHECK (from_currency_id != to_currency_id) ); -- Tabla: uom_categories (Categorías de unidades de medida) CREATE TABLE core.uom_categories ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), name VARCHAR(100) NOT NULL UNIQUE, description TEXT, -- Sin tenant_id: catálogo global created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ); -- Tabla: uom (Unidades de medida) CREATE TABLE core.uom ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), category_id UUID NOT NULL REFERENCES core.uom_categories(id), name VARCHAR(100) NOT NULL, code VARCHAR(20), uom_type core.uom_type NOT NULL DEFAULT 'reference', factor DECIMAL(12, 6) NOT NULL DEFAULT 1.0, rounding DECIMAL(12, 6) DEFAULT 0.01, active BOOLEAN NOT NULL DEFAULT TRUE, -- Sin tenant_id: catálogo global created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, CONSTRAINT uq_uom_name_category UNIQUE (category_id, name), CONSTRAINT chk_uom_factor CHECK (factor > 0) ); -- Tabla: partners (Partners universales - patrón Odoo) CREATE TABLE core.partners ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, -- Datos básicos name VARCHAR(255) NOT NULL, legal_name VARCHAR(255), partner_type core.partner_type NOT NULL DEFAULT 'person', -- Categorización (multiple flags como Odoo) is_customer BOOLEAN DEFAULT FALSE, is_supplier BOOLEAN DEFAULT FALSE, is_employee BOOLEAN DEFAULT FALSE, is_company BOOLEAN DEFAULT FALSE, -- Contacto email VARCHAR(255), phone VARCHAR(50), mobile VARCHAR(50), website VARCHAR(255), -- Fiscal tax_id VARCHAR(50), -- RFC en México -- Referencias company_id UUID REFERENCES auth.companies(id), parent_id UUID REFERENCES core.partners(id), -- Para jerarquía de contactos user_id UUID REFERENCES auth.users(id), -- Usuario vinculado (si aplica) -- Comercial payment_term_id UUID, -- FK a financial.payment_terms (se crea después) pricelist_id UUID, -- FK a sales.pricelists (se crea después) -- Configuración language VARCHAR(10) DEFAULT 'es', currency_id UUID REFERENCES core.currencies(id), -- Notas notes TEXT, internal_notes TEXT, -- Control active BOOLEAN NOT NULL DEFAULT TRUE, -- Auditoría created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, created_by UUID REFERENCES auth.users(id), updated_at TIMESTAMP, updated_by UUID REFERENCES auth.users(id), deleted_at TIMESTAMP, deleted_by UUID REFERENCES auth.users(id), CONSTRAINT chk_partners_email_format CHECK ( email IS NULL OR email ~ '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}$' ), CONSTRAINT chk_partners_no_self_parent CHECK (id != parent_id) ); -- Tabla: addresses (Direcciones de partners) CREATE TABLE core.addresses ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), partner_id UUID NOT NULL REFERENCES core.partners(id) ON DELETE CASCADE, -- Tipo de dirección address_type core.address_type NOT NULL DEFAULT 'contact', -- Dirección street VARCHAR(255), street2 VARCHAR(255), city VARCHAR(100), state VARCHAR(100), zip_code VARCHAR(20), country_id UUID REFERENCES core.countries(id), -- Control is_default BOOLEAN DEFAULT FALSE, active BOOLEAN NOT NULL DEFAULT TRUE, -- Auditoría created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, created_by UUID REFERENCES auth.users(id), updated_at TIMESTAMP, updated_by UUID REFERENCES auth.users(id), deleted_at TIMESTAMP, deleted_by UUID REFERENCES auth.users(id) ); -- Tabla: product_categories (Categorías de productos) CREATE TABLE core.product_categories ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, name VARCHAR(255) NOT NULL, code VARCHAR(50), parent_id UUID REFERENCES core.product_categories(id), full_path TEXT, -- Generado automáticamente: "Electrónica / Computadoras / Laptops" -- Configuración notes TEXT, active BOOLEAN NOT NULL DEFAULT TRUE, -- Auditoría created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, created_by UUID REFERENCES auth.users(id), updated_at TIMESTAMP, updated_by UUID REFERENCES auth.users(id), deleted_at TIMESTAMP, deleted_by UUID REFERENCES auth.users(id), CONSTRAINT uq_product_categories_code_tenant UNIQUE (tenant_id, code), CONSTRAINT chk_product_categories_no_self_parent CHECK (id != parent_id) ); -- Tabla: tags (Etiquetas genéricas) CREATE TABLE core.tags ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, name VARCHAR(100) NOT NULL, color VARCHAR(20), -- Color hex: #FF5733 model VARCHAR(100), -- Para qué se usa: 'products', 'partners', 'tasks', etc. description TEXT, -- Auditoría created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, created_by UUID REFERENCES auth.users(id), CONSTRAINT uq_tags_name_model_tenant UNIQUE (tenant_id, name, model) ); -- Tabla: sequences (Generación de números secuenciales) CREATE TABLE core.sequences ( 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), code VARCHAR(100) NOT NULL, -- Código único: 'sale.order', 'purchase.order', etc. name VARCHAR(255) NOT NULL, prefix VARCHAR(50), -- Prefijo: "SO-", "PO-", etc. suffix VARCHAR(50), -- Sufijo: "/2025" next_number INTEGER NOT NULL DEFAULT 1, padding INTEGER NOT NULL DEFAULT 4, -- 0001, 0002, etc. -- 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_sequences_code_tenant UNIQUE (tenant_id, code), CONSTRAINT chk_sequences_next_number CHECK (next_number > 0), CONSTRAINT chk_sequences_padding CHECK (padding >= 0) ); -- Tabla: attachments (Archivos adjuntos genéricos) CREATE TABLE core.attachments ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, -- Referencia polimórfica (a qué tabla/registro pertenece) model VARCHAR(100) NOT NULL, -- 'partners', 'invoices', 'tasks', etc. record_id UUID NOT NULL, -- Archivo filename VARCHAR(255) NOT NULL, mimetype VARCHAR(100), size_bytes BIGINT, url VARCHAR(1000), -- URL en S3, local storage, etc. -- Metadatos description TEXT, is_public BOOLEAN DEFAULT FALSE, -- Auditoría created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, created_by UUID REFERENCES auth.users(id), deleted_at TIMESTAMP, deleted_by UUID REFERENCES auth.users(id), CONSTRAINT chk_attachments_size CHECK (size_bytes >= 0) ); -- Tabla: notes (Notas genéricas) CREATE TABLE core.notes ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, -- Referencia polimórfica model VARCHAR(100) NOT NULL, record_id UUID NOT NULL, -- Nota subject VARCHAR(255), content TEXT NOT NULL, -- Control is_pinned BOOLEAN DEFAULT FALSE, -- Auditoría created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, created_by UUID REFERENCES auth.users(id), updated_at TIMESTAMP, updated_by UUID REFERENCES auth.users(id), deleted_at TIMESTAMP, deleted_by UUID REFERENCES auth.users(id) ); -- ===================================================== -- INDICES -- ===================================================== -- Countries CREATE INDEX idx_countries_code ON core.countries(code); CREATE INDEX idx_countries_name ON core.countries(name); -- Currencies CREATE INDEX idx_currencies_code ON core.currencies(code); CREATE INDEX idx_currencies_active ON core.currencies(active) WHERE active = TRUE; -- Exchange Rates CREATE INDEX idx_exchange_rates_from_currency ON core.exchange_rates(from_currency_id); CREATE INDEX idx_exchange_rates_to_currency ON core.exchange_rates(to_currency_id); CREATE INDEX idx_exchange_rates_date ON core.exchange_rates(date DESC); -- UoM Categories CREATE INDEX idx_uom_categories_name ON core.uom_categories(name); -- UoM CREATE INDEX idx_uom_category_id ON core.uom(category_id); CREATE INDEX idx_uom_active ON core.uom(active) WHERE active = TRUE; -- Partners CREATE INDEX idx_partners_tenant_id ON core.partners(tenant_id); CREATE INDEX idx_partners_name ON core.partners(name); CREATE INDEX idx_partners_email ON core.partners(email); CREATE INDEX idx_partners_tax_id ON core.partners(tax_id); CREATE INDEX idx_partners_parent_id ON core.partners(parent_id); CREATE INDEX idx_partners_user_id ON core.partners(user_id); CREATE INDEX idx_partners_company_id ON core.partners(company_id); CREATE INDEX idx_partners_currency_id ON core.partners(currency_id) WHERE currency_id IS NOT NULL; CREATE INDEX idx_partners_payment_term_id ON core.partners(payment_term_id) WHERE payment_term_id IS NOT NULL; CREATE INDEX idx_partners_pricelist_id ON core.partners(pricelist_id) WHERE pricelist_id IS NOT NULL; CREATE INDEX idx_partners_is_customer ON core.partners(tenant_id, is_customer) WHERE is_customer = TRUE; CREATE INDEX idx_partners_is_supplier ON core.partners(tenant_id, is_supplier) WHERE is_supplier = TRUE; CREATE INDEX idx_partners_is_employee ON core.partners(tenant_id, is_employee) WHERE is_employee = TRUE; CREATE INDEX idx_partners_active ON core.partners(tenant_id, active) WHERE active = TRUE; -- Addresses CREATE INDEX idx_addresses_partner_id ON core.addresses(partner_id); CREATE INDEX idx_addresses_country_id ON core.addresses(country_id); CREATE INDEX idx_addresses_is_default ON core.addresses(partner_id, is_default) WHERE is_default = TRUE; -- Product Categories CREATE INDEX idx_product_categories_tenant_id ON core.product_categories(tenant_id); CREATE INDEX idx_product_categories_parent_id ON core.product_categories(parent_id); CREATE INDEX idx_product_categories_code ON core.product_categories(code); -- Tags CREATE INDEX idx_tags_tenant_id ON core.tags(tenant_id); CREATE INDEX idx_tags_model ON core.tags(model); CREATE INDEX idx_tags_name ON core.tags(name); -- Sequences CREATE INDEX idx_sequences_tenant_id ON core.sequences(tenant_id); CREATE INDEX idx_sequences_code ON core.sequences(code); -- Attachments CREATE INDEX idx_attachments_tenant_id ON core.attachments(tenant_id); CREATE INDEX idx_attachments_model_record ON core.attachments(model, record_id); CREATE INDEX idx_attachments_created_by ON core.attachments(created_by); -- Notes CREATE INDEX idx_notes_tenant_id ON core.notes(tenant_id); CREATE INDEX idx_notes_model_record ON core.notes(model, record_id); CREATE INDEX idx_notes_created_by ON core.notes(created_by); CREATE INDEX idx_notes_is_pinned ON core.notes(is_pinned) WHERE is_pinned = TRUE; -- ===================================================== -- FUNCTIONS -- ===================================================== -- Función: generate_next_sequence -- Genera el siguiente número de secuencia CREATE OR REPLACE FUNCTION core.generate_next_sequence(p_sequence_code VARCHAR) RETURNS VARCHAR AS $$ DECLARE v_sequence RECORD; v_next_number INTEGER; v_result VARCHAR; BEGIN -- Obtener secuencia y bloquear fila (SELECT FOR UPDATE) SELECT * INTO v_sequence FROM core.sequences WHERE code = p_sequence_code AND tenant_id = get_current_tenant_id() FOR UPDATE; IF NOT FOUND THEN RAISE EXCEPTION 'Sequence % not found', p_sequence_code; END IF; -- Generar número v_next_number := v_sequence.next_number; -- Formatear resultado v_result := COALESCE(v_sequence.prefix, '') || LPAD(v_next_number::TEXT, v_sequence.padding, '0') || COALESCE(v_sequence.suffix, ''); -- Incrementar contador UPDATE core.sequences SET next_number = next_number + 1, updated_at = CURRENT_TIMESTAMP, updated_by = get_current_user_id() WHERE id = v_sequence.id; RETURN v_result; END; $$ LANGUAGE plpgsql; COMMENT ON FUNCTION core.generate_next_sequence IS 'Genera el siguiente número de secuencia para un código dado'; -- Función: update_product_category_path -- Actualiza el full_path de una categoría de producto CREATE OR REPLACE FUNCTION core.update_product_category_path() RETURNS TRIGGER AS $$ DECLARE v_parent_path TEXT; BEGIN IF NEW.parent_id IS NULL THEN NEW.full_path := NEW.name; ELSE SELECT full_path INTO v_parent_path FROM core.product_categories WHERE id = NEW.parent_id; NEW.full_path := v_parent_path || ' / ' || NEW.name; END IF; RETURN NEW; END; $$ LANGUAGE plpgsql; COMMENT ON FUNCTION core.update_product_category_path IS 'Actualiza el path completo de la categoría al crear/actualizar'; -- Función: get_exchange_rate -- Obtiene la tasa de cambio entre dos monedas en una fecha CREATE OR REPLACE FUNCTION core.get_exchange_rate( p_from_currency_id UUID, p_to_currency_id UUID, p_date DATE DEFAULT CURRENT_DATE ) RETURNS DECIMAL AS $$ DECLARE v_rate DECIMAL; BEGIN -- Si son la misma moneda, tasa = 1 IF p_from_currency_id = p_to_currency_id THEN RETURN 1.0; END IF; -- Buscar tasa directa SELECT rate INTO v_rate FROM core.exchange_rates WHERE from_currency_id = p_from_currency_id AND to_currency_id = p_to_currency_id AND date <= p_date ORDER BY date DESC LIMIT 1; IF FOUND THEN RETURN v_rate; END IF; -- Buscar tasa inversa SELECT 1.0 / rate INTO v_rate FROM core.exchange_rates WHERE from_currency_id = p_to_currency_id AND to_currency_id = p_from_currency_id AND date <= p_date ORDER BY date DESC LIMIT 1; IF FOUND THEN RETURN v_rate; END IF; -- No se encontró tasa RAISE EXCEPTION 'Exchange rate not found for currencies % to % on date %', p_from_currency_id, p_to_currency_id, p_date; END; $$ LANGUAGE plpgsql STABLE; COMMENT ON FUNCTION core.get_exchange_rate IS 'Obtiene la tasa de cambio entre dos monedas en una fecha específica'; -- ===================================================== -- TRIGGERS -- ===================================================== -- Trigger: Actualizar updated_at en partners CREATE TRIGGER trg_partners_updated_at BEFORE UPDATE ON core.partners FOR EACH ROW EXECUTE FUNCTION auth.update_updated_at_column(); -- Trigger: Actualizar updated_at en addresses CREATE TRIGGER trg_addresses_updated_at BEFORE UPDATE ON core.addresses FOR EACH ROW EXECUTE FUNCTION auth.update_updated_at_column(); -- Trigger: Actualizar updated_at en product_categories CREATE TRIGGER trg_product_categories_updated_at BEFORE UPDATE ON core.product_categories FOR EACH ROW EXECUTE FUNCTION auth.update_updated_at_column(); -- Trigger: Actualizar updated_at en notes CREATE TRIGGER trg_notes_updated_at BEFORE UPDATE ON core.notes FOR EACH ROW EXECUTE FUNCTION auth.update_updated_at_column(); -- Trigger: Actualizar full_path en product_categories CREATE TRIGGER trg_product_categories_update_path BEFORE INSERT OR UPDATE OF name, parent_id ON core.product_categories FOR EACH ROW EXECUTE FUNCTION core.update_product_category_path(); -- ===================================================== -- ROW LEVEL SECURITY (RLS) -- ===================================================== -- Habilitar RLS en tablas con tenant_id ALTER TABLE core.partners ENABLE ROW LEVEL SECURITY; ALTER TABLE core.product_categories ENABLE ROW LEVEL SECURITY; ALTER TABLE core.tags ENABLE ROW LEVEL SECURITY; ALTER TABLE core.sequences ENABLE ROW LEVEL SECURITY; ALTER TABLE core.attachments ENABLE ROW LEVEL SECURITY; ALTER TABLE core.notes ENABLE ROW LEVEL SECURITY; -- Policy: Tenant Isolation - Partners CREATE POLICY tenant_isolation_partners ON core.partners USING (tenant_id = get_current_tenant_id()); -- Policy: Tenant Isolation - Product Categories CREATE POLICY tenant_isolation_product_categories ON core.product_categories USING (tenant_id = get_current_tenant_id()); -- Policy: Tenant Isolation - Tags CREATE POLICY tenant_isolation_tags ON core.tags USING (tenant_id = get_current_tenant_id()); -- Policy: Tenant Isolation - Sequences CREATE POLICY tenant_isolation_sequences ON core.sequences USING (tenant_id = get_current_tenant_id()); -- Policy: Tenant Isolation - Attachments CREATE POLICY tenant_isolation_attachments ON core.attachments USING (tenant_id = get_current_tenant_id()); -- Policy: Tenant Isolation - Notes CREATE POLICY tenant_isolation_notes ON core.notes USING (tenant_id = get_current_tenant_id()); -- ===================================================== -- SEED DATA -- ===================================================== -- Monedas principales (ISO 4217) INSERT INTO core.currencies (code, name, symbol, decimals) VALUES ('USD', 'US Dollar', '$', 2), ('MXN', 'Peso Mexicano', '$', 2), ('EUR', 'Euro', '€', 2), ('GBP', 'British Pound', '£', 2), ('CAD', 'Canadian Dollar', '$', 2), ('JPY', 'Japanese Yen', '¥', 0), ('CNY', 'Chinese Yuan', '¥', 2), ('BRL', 'Brazilian Real', 'R$', 2), ('ARS', 'Argentine Peso', '$', 2), ('COP', 'Colombian Peso', '$', 2) ON CONFLICT (code) DO NOTHING; -- Países principales (ISO 3166-1) INSERT INTO core.countries (code, name, phone_code, currency_code) VALUES ('MX', 'México', '52', 'MXN'), ('US', 'United States', '1', 'USD'), ('CA', 'Canada', '1', 'CAD'), ('GB', 'United Kingdom', '44', 'GBP'), ('FR', 'France', '33', 'EUR'), ('DE', 'Germany', '49', 'EUR'), ('ES', 'Spain', '34', 'EUR'), ('IT', 'Italy', '39', 'EUR'), ('BR', 'Brazil', '55', 'BRL'), ('AR', 'Argentina', '54', 'ARS'), ('CO', 'Colombia', '57', 'COP'), ('CL', 'Chile', '56', 'CLP'), ('PE', 'Peru', '51', 'PEN'), ('CN', 'China', '86', 'CNY'), ('JP', 'Japan', '81', 'JPY'), ('IN', 'India', '91', 'INR') ON CONFLICT (code) DO NOTHING; -- Categorías de UoM INSERT INTO core.uom_categories (name, description) VALUES ('Weight', 'Unidades de peso'), ('Volume', 'Unidades de volumen'), ('Length', 'Unidades de longitud'), ('Time', 'Unidades de tiempo'), ('Unit', 'Unidades (piezas, docenas, etc.)') ON CONFLICT (name) DO NOTHING; -- Unidades de medida estándar INSERT INTO core.uom (category_id, name, code, uom_type, factor) SELECT cat.id, uom.name, uom.code, uom.uom_type::core.uom_type, uom.factor FROM ( -- Weight SELECT (SELECT id FROM core.uom_categories WHERE name = 'Weight'), 'Kilogram', 'kg', 'reference', 1.0 UNION ALL SELECT (SELECT id FROM core.uom_categories WHERE name = 'Weight'), 'Gram', 'g', 'smaller', 0.001 UNION ALL SELECT (SELECT id FROM core.uom_categories WHERE name = 'Weight'), 'Ton', 't', 'bigger', 1000.0 UNION ALL SELECT (SELECT id FROM core.uom_categories WHERE name = 'Weight'), 'Pound', 'lb', 'smaller', 0.453592 UNION ALL -- Volume SELECT (SELECT id FROM core.uom_categories WHERE name = 'Volume'), 'Liter', 'L', 'reference', 1.0 UNION ALL SELECT (SELECT id FROM core.uom_categories WHERE name = 'Volume'), 'Milliliter', 'mL', 'smaller', 0.001 UNION ALL SELECT (SELECT id FROM core.uom_categories WHERE name = 'Volume'), 'Cubic Meter', 'm³', 'bigger', 1000.0 UNION ALL SELECT (SELECT id FROM core.uom_categories WHERE name = 'Volume'), 'Gallon', 'gal', 'bigger', 3.78541 UNION ALL -- Length SELECT (SELECT id FROM core.uom_categories WHERE name = 'Length'), 'Meter', 'm', 'reference', 1.0 UNION ALL SELECT (SELECT id FROM core.uom_categories WHERE name = 'Length'), 'Centimeter', 'cm', 'smaller', 0.01 UNION ALL SELECT (SELECT id FROM core.uom_categories WHERE name = 'Length'), 'Millimeter', 'mm', 'smaller', 0.001 UNION ALL SELECT (SELECT id FROM core.uom_categories WHERE name = 'Length'), 'Kilometer', 'km', 'bigger', 1000.0 UNION ALL SELECT (SELECT id FROM core.uom_categories WHERE name = 'Length'), 'Inch', 'in', 'smaller', 0.0254 UNION ALL SELECT (SELECT id FROM core.uom_categories WHERE name = 'Length'), 'Foot', 'ft', 'smaller', 0.3048 UNION ALL -- Time SELECT (SELECT id FROM core.uom_categories WHERE name = 'Time'), 'Hour', 'h', 'reference', 1.0 UNION ALL SELECT (SELECT id FROM core.uom_categories WHERE name = 'Time'), 'Day', 'd', 'bigger', 24.0 UNION ALL SELECT (SELECT id FROM core.uom_categories WHERE name = 'Time'), 'Week', 'wk', 'bigger', 168.0 UNION ALL -- Unit SELECT (SELECT id FROM core.uom_categories WHERE name = 'Unit'), 'Unit', 'unit', 'reference', 1.0 UNION ALL SELECT (SELECT id FROM core.uom_categories WHERE name = 'Unit'), 'Dozen', 'doz', 'bigger', 12.0 UNION ALL SELECT (SELECT id FROM core.uom_categories WHERE name = 'Unit'), 'Pack', 'pack', 'bigger', 1.0 ) AS uom(category_id, name, code, uom_type, factor) JOIN core.uom_categories cat ON cat.id = uom.category_id ON CONFLICT DO NOTHING; -- ===================================================== -- COMENTARIOS EN TABLAS -- ===================================================== COMMENT ON SCHEMA core IS 'Schema de catálogos maestros y entidades fundamentales'; COMMENT ON TABLE core.countries IS 'Catálogo de países (ISO 3166-1)'; COMMENT ON TABLE core.currencies IS 'Catálogo de monedas (ISO 4217)'; COMMENT ON TABLE core.exchange_rates IS 'Tasas de cambio históricas entre monedas'; COMMENT ON TABLE core.uom_categories IS 'Categorías de unidades de medida'; COMMENT ON TABLE core.uom IS 'Unidades de medida (peso, volumen, longitud, etc.)'; COMMENT ON TABLE core.partners IS 'Partners universales (clientes, proveedores, empleados, contactos) - patrón Odoo'; COMMENT ON TABLE core.addresses IS 'Direcciones de partners (facturación, envío, contacto)'; COMMENT ON TABLE core.product_categories IS 'Categorías jerárquicas de productos'; COMMENT ON TABLE core.tags IS 'Etiquetas genéricas para clasificar registros'; COMMENT ON TABLE core.sequences IS 'Generadores de números secuenciales automáticos'; COMMENT ON TABLE core.attachments IS 'Archivos adjuntos polimórficos (cualquier tabla/registro)'; COMMENT ON TABLE core.notes IS 'Notas polimórficas (cualquier tabla/registro)'; -- ===================================================== -- VISTAS ÚTILES -- ===================================================== -- Vista: customers (solo partners que son clientes) CREATE OR REPLACE VIEW core.customers_view AS SELECT id, tenant_id, name, legal_name, email, phone, mobile, tax_id, company_id, active FROM core.partners WHERE is_customer = TRUE AND deleted_at IS NULL; COMMENT ON VIEW core.customers_view IS 'Vista de partners que son clientes'; -- Vista: suppliers (solo partners que son proveedores) CREATE OR REPLACE VIEW core.suppliers_view AS SELECT id, tenant_id, name, legal_name, email, phone, tax_id, company_id, active FROM core.partners WHERE is_supplier = TRUE AND deleted_at IS NULL; COMMENT ON VIEW core.suppliers_view IS 'Vista de partners que son proveedores'; -- Vista: employees (solo partners que son empleados) CREATE OR REPLACE VIEW core.employees_view AS SELECT p.id, p.tenant_id, p.name, p.email, p.phone, p.user_id, u.full_name as user_name, p.active FROM core.partners p LEFT JOIN auth.users u ON p.user_id = u.id WHERE p.is_employee = TRUE AND p.deleted_at IS NULL; COMMENT ON VIEW core.employees_view IS 'Vista de partners que son empleados'; -- ===================================================== -- FIN DEL SCHEMA CORE -- =====================================================