753 lines
25 KiB
PL/PgSQL
753 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_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
|
|
-- =====================================================
|