🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1053 lines
35 KiB
PL/PgSQL
1053 lines
35 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', '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';
|
|
|
|
-- =====================================================
|
|
-- COR-020: Duplicate Detection (Partners)
|
|
-- Sistema de deteccion de duplicados
|
|
-- =====================================================
|
|
|
|
-- Tabla: partner_duplicates (Posibles duplicados detectados)
|
|
CREATE TABLE core.partner_duplicates (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE,
|
|
|
|
-- Partners involucrados
|
|
partner1_id UUID NOT NULL REFERENCES core.partners(id) ON DELETE CASCADE,
|
|
partner2_id UUID NOT NULL REFERENCES core.partners(id) ON DELETE CASCADE,
|
|
|
|
-- Puntuacion de similitud (0-100)
|
|
similarity_score INTEGER NOT NULL,
|
|
|
|
-- Campos que coinciden
|
|
matching_fields JSONB DEFAULT '{}', -- {"email": true, "phone": true, "name_similarity": 0.85}
|
|
|
|
-- Estado
|
|
status VARCHAR(20) DEFAULT 'pending', -- pending, merged, ignored, false_positive
|
|
|
|
-- Resolucion
|
|
resolved_at TIMESTAMP,
|
|
resolved_by UUID REFERENCES auth.users(id),
|
|
resolution_notes TEXT,
|
|
|
|
-- Auditoria
|
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
|
|
CONSTRAINT uq_partner_duplicates UNIQUE (tenant_id, partner1_id, partner2_id),
|
|
CONSTRAINT chk_partner_duplicates_different CHECK (partner1_id != partner2_id),
|
|
CONSTRAINT chk_partner_duplicates_score CHECK (similarity_score >= 0 AND similarity_score <= 100),
|
|
CONSTRAINT chk_partner_duplicates_status CHECK (status IN ('pending', 'merged', 'ignored', 'false_positive'))
|
|
);
|
|
|
|
-- Indices para partner_duplicates
|
|
CREATE INDEX idx_partner_duplicates_tenant ON core.partner_duplicates(tenant_id);
|
|
CREATE INDEX idx_partner_duplicates_partner1 ON core.partner_duplicates(partner1_id);
|
|
CREATE INDEX idx_partner_duplicates_partner2 ON core.partner_duplicates(partner2_id);
|
|
CREATE INDEX idx_partner_duplicates_status ON core.partner_duplicates(status);
|
|
CREATE INDEX idx_partner_duplicates_score ON core.partner_duplicates(similarity_score DESC);
|
|
|
|
-- RLS para partner_duplicates
|
|
ALTER TABLE core.partner_duplicates ENABLE ROW LEVEL SECURITY;
|
|
CREATE POLICY tenant_isolation_partner_duplicates ON core.partner_duplicates
|
|
USING (tenant_id = get_current_tenant_id());
|
|
|
|
-- Funcion: calculate_partner_similarity
|
|
CREATE OR REPLACE FUNCTION core.calculate_partner_similarity(
|
|
p_partner1_id UUID,
|
|
p_partner2_id UUID
|
|
)
|
|
RETURNS TABLE(
|
|
similarity_score INTEGER,
|
|
matching_fields JSONB
|
|
) AS $$
|
|
DECLARE
|
|
v_p1 RECORD;
|
|
v_p2 RECORD;
|
|
v_score INTEGER := 0;
|
|
v_matches JSONB := '{}';
|
|
v_name_similarity DECIMAL;
|
|
BEGIN
|
|
-- Obtener partners
|
|
SELECT * INTO v_p1 FROM core.partners WHERE id = p_partner1_id;
|
|
SELECT * INTO v_p2 FROM core.partners WHERE id = p_partner2_id;
|
|
|
|
IF NOT FOUND THEN
|
|
RETURN QUERY SELECT 0::INTEGER, '{}'::JSONB;
|
|
RETURN;
|
|
END IF;
|
|
|
|
-- Verificar email (40 puntos)
|
|
IF v_p1.email IS NOT NULL AND v_p2.email IS NOT NULL THEN
|
|
IF LOWER(v_p1.email) = LOWER(v_p2.email) THEN
|
|
v_score := v_score + 40;
|
|
v_matches := v_matches || '{"email": true}';
|
|
END IF;
|
|
END IF;
|
|
|
|
-- Verificar telefono (20 puntos)
|
|
IF v_p1.phone IS NOT NULL AND v_p2.phone IS NOT NULL THEN
|
|
IF regexp_replace(v_p1.phone, '[^0-9]', '', 'g') = regexp_replace(v_p2.phone, '[^0-9]', '', 'g') THEN
|
|
v_score := v_score + 20;
|
|
v_matches := v_matches || '{"phone": true}';
|
|
END IF;
|
|
END IF;
|
|
|
|
-- Verificar tax_id (30 puntos)
|
|
IF v_p1.tax_id IS NOT NULL AND v_p2.tax_id IS NOT NULL THEN
|
|
IF UPPER(v_p1.tax_id) = UPPER(v_p2.tax_id) THEN
|
|
v_score := v_score + 30;
|
|
v_matches := v_matches || '{"tax_id": true}';
|
|
END IF;
|
|
END IF;
|
|
|
|
-- Verificar nombre (similarity usando trigramas - hasta 30 puntos)
|
|
-- Usamos una comparacion simple si no hay pg_trgm
|
|
IF v_p1.name IS NOT NULL AND v_p2.name IS NOT NULL THEN
|
|
IF LOWER(v_p1.name) = LOWER(v_p2.name) THEN
|
|
v_score := v_score + 30;
|
|
v_matches := v_matches || '{"name_exact": true}';
|
|
ELSIF LOWER(v_p1.name) LIKE '%' || LOWER(v_p2.name) || '%'
|
|
OR LOWER(v_p2.name) LIKE '%' || LOWER(v_p1.name) || '%' THEN
|
|
v_score := v_score + 15;
|
|
v_matches := v_matches || '{"name_partial": true}';
|
|
END IF;
|
|
END IF;
|
|
|
|
-- Normalizar score a 100 maximo
|
|
v_score := LEAST(v_score, 100);
|
|
|
|
RETURN QUERY SELECT v_score, v_matches;
|
|
END;
|
|
$$ LANGUAGE plpgsql STABLE;
|
|
|
|
COMMENT ON FUNCTION core.calculate_partner_similarity IS
|
|
'COR-020: Calcula la similitud entre dos partners para deteccion de duplicados';
|
|
|
|
-- Funcion: find_partner_duplicates
|
|
CREATE OR REPLACE FUNCTION core.find_partner_duplicates(
|
|
p_partner_id UUID,
|
|
p_min_score INTEGER DEFAULT 50
|
|
)
|
|
RETURNS TABLE(
|
|
partner_id UUID,
|
|
partner_name VARCHAR,
|
|
similarity_score INTEGER,
|
|
matching_fields JSONB
|
|
) AS $$
|
|
DECLARE
|
|
v_partner RECORD;
|
|
v_candidate RECORD;
|
|
v_result RECORD;
|
|
BEGIN
|
|
-- Obtener partner
|
|
SELECT * INTO v_partner FROM core.partners WHERE id = p_partner_id;
|
|
|
|
IF NOT FOUND THEN
|
|
RETURN;
|
|
END IF;
|
|
|
|
-- Buscar candidatos
|
|
FOR v_candidate IN
|
|
SELECT * FROM core.partners
|
|
WHERE id != p_partner_id
|
|
AND tenant_id = v_partner.tenant_id
|
|
AND deleted_at IS NULL
|
|
AND active = TRUE
|
|
-- Pre-filtro para eficiencia
|
|
AND (
|
|
email = v_partner.email
|
|
OR phone = v_partner.phone
|
|
OR tax_id = v_partner.tax_id
|
|
OR name ILIKE '%' || v_partner.name || '%'
|
|
OR v_partner.name ILIKE '%' || name || '%'
|
|
)
|
|
LOOP
|
|
SELECT * INTO v_result
|
|
FROM core.calculate_partner_similarity(p_partner_id, v_candidate.id);
|
|
|
|
IF v_result.similarity_score >= p_min_score THEN
|
|
RETURN QUERY SELECT
|
|
v_candidate.id,
|
|
v_candidate.name,
|
|
v_result.similarity_score,
|
|
v_result.matching_fields;
|
|
END IF;
|
|
END LOOP;
|
|
END;
|
|
$$ LANGUAGE plpgsql STABLE;
|
|
|
|
COMMENT ON FUNCTION core.find_partner_duplicates IS
|
|
'COR-020: Busca posibles duplicados de un partner con score minimo configurable';
|
|
|
|
-- Funcion: auto_detect_duplicates_on_create
|
|
CREATE OR REPLACE FUNCTION core.auto_detect_duplicates_on_create()
|
|
RETURNS TRIGGER AS $$
|
|
DECLARE
|
|
v_duplicate RECORD;
|
|
BEGIN
|
|
-- Solo buscar duplicados si hay datos suficientes
|
|
IF NEW.email IS NOT NULL OR NEW.phone IS NOT NULL OR NEW.tax_id IS NOT NULL THEN
|
|
FOR v_duplicate IN
|
|
SELECT * FROM core.find_partner_duplicates(NEW.id, 60)
|
|
LOOP
|
|
-- Insertar en tabla de duplicados (si no existe)
|
|
INSERT INTO core.partner_duplicates (
|
|
tenant_id, partner1_id, partner2_id,
|
|
similarity_score, matching_fields
|
|
) VALUES (
|
|
NEW.tenant_id,
|
|
LEAST(NEW.id, v_duplicate.partner_id),
|
|
GREATEST(NEW.id, v_duplicate.partner_id),
|
|
v_duplicate.similarity_score,
|
|
v_duplicate.matching_fields
|
|
) ON CONFLICT (tenant_id, partner1_id, partner2_id) DO UPDATE
|
|
SET similarity_score = EXCLUDED.similarity_score,
|
|
matching_fields = EXCLUDED.matching_fields;
|
|
END LOOP;
|
|
END IF;
|
|
|
|
RETURN NEW;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
COMMENT ON FUNCTION core.auto_detect_duplicates_on_create IS
|
|
'COR-020: Trigger para detectar duplicados automaticamente al crear partner';
|
|
|
|
-- Trigger: Detectar duplicados al crear partner
|
|
CREATE TRIGGER trg_partners_detect_duplicates
|
|
AFTER INSERT ON core.partners
|
|
FOR EACH ROW
|
|
EXECUTE FUNCTION core.auto_detect_duplicates_on_create();
|
|
|
|
COMMENT ON TABLE core.partner_duplicates IS 'COR-020: Posibles duplicados de partners detectados';
|
|
|
|
-- =====================================================
|
|
-- COR-021: States/Provinces
|
|
-- Equivalente a res.country.state de Odoo
|
|
-- =====================================================
|
|
|
|
CREATE TABLE core.states (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
country_id UUID NOT NULL REFERENCES core.countries(id) ON DELETE CASCADE,
|
|
name VARCHAR(100) NOT NULL,
|
|
code VARCHAR(10) NOT NULL,
|
|
is_active BOOLEAN DEFAULT TRUE,
|
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
|
|
UNIQUE(country_id, code)
|
|
);
|
|
|
|
CREATE INDEX idx_states_country ON core.states(country_id);
|
|
CREATE INDEX idx_states_name ON core.states(name);
|
|
|
|
COMMENT ON TABLE core.states IS 'COR-021: States/Provinces - Equivalent to res.country.state';
|
|
|
|
-- Agregar state_id a partners y addresses
|
|
ALTER TABLE core.partners ADD COLUMN IF NOT EXISTS state_id UUID REFERENCES core.states(id);
|
|
ALTER TABLE core.addresses ADD COLUMN IF NOT EXISTS state_id UUID REFERENCES core.states(id);
|
|
|
|
-- =====================================================
|
|
-- COR-022: Banks and Partner Bank Accounts
|
|
-- Equivalente a res.bank y res.partner.bank de Odoo
|
|
-- =====================================================
|
|
|
|
-- Tabla: banks (Catalogo de bancos)
|
|
CREATE TABLE core.banks (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
name VARCHAR(255) NOT NULL,
|
|
bic VARCHAR(11), -- SWIFT/BIC code
|
|
country_id UUID REFERENCES core.countries(id),
|
|
street VARCHAR(255),
|
|
city VARCHAR(100),
|
|
zip VARCHAR(20),
|
|
phone VARCHAR(50),
|
|
email VARCHAR(255),
|
|
is_active BOOLEAN DEFAULT TRUE,
|
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
|
|
CREATE INDEX idx_banks_country ON core.banks(country_id);
|
|
CREATE UNIQUE INDEX idx_banks_bic ON core.banks(bic) WHERE bic IS NOT NULL;
|
|
|
|
COMMENT ON TABLE core.banks IS 'COR-022: Banks catalog - Equivalent to res.bank';
|
|
|
|
-- Tabla: partner_banks (Cuentas bancarias de partners)
|
|
CREATE TABLE core.partner_banks (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE,
|
|
partner_id UUID NOT NULL REFERENCES core.partners(id) ON DELETE CASCADE,
|
|
bank_id UUID REFERENCES core.banks(id),
|
|
acc_number VARCHAR(64) NOT NULL,
|
|
acc_holder_name VARCHAR(255),
|
|
sequence INTEGER DEFAULT 10,
|
|
currency_id UUID REFERENCES core.currencies(id),
|
|
is_active BOOLEAN DEFAULT TRUE,
|
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
|
|
CREATE INDEX idx_partner_banks_tenant ON core.partner_banks(tenant_id);
|
|
CREATE INDEX idx_partner_banks_partner ON core.partner_banks(partner_id);
|
|
CREATE INDEX idx_partner_banks_bank ON core.partner_banks(bank_id);
|
|
|
|
-- RLS para partner_banks
|
|
ALTER TABLE core.partner_banks ENABLE ROW LEVEL SECURITY;
|
|
CREATE POLICY tenant_isolation_partner_banks ON core.partner_banks
|
|
USING (tenant_id = get_current_tenant_id());
|
|
|
|
COMMENT ON TABLE core.partner_banks IS 'COR-022: Partner bank accounts - Equivalent to res.partner.bank';
|
|
|
|
-- =====================================================
|
|
-- FIN DEL SCHEMA CORE
|
|
-- =====================================================
|