erp-core-database-v2/ddl/02-core.sql

756 lines
25 KiB
PL/PgSQL

-- =====================================================
-- 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', '', '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
-- =====================================================