diff --git a/ddl/20-core-catalogs.sql b/ddl/20-core-catalogs.sql new file mode 100644 index 0000000..976825b --- /dev/null +++ b/ddl/20-core-catalogs.sql @@ -0,0 +1,467 @@ +-- ============================================================= +-- ARCHIVO: 20-core-catalogs.sql +-- DESCRIPCION: Catalogos maestros - paises, estados, monedas, UoM +-- VERSION: 1.0.0 +-- PROYECTO: ERP-Core V2 +-- FECHA: 2026-01-18 +-- ============================================================= + +-- ===================== +-- SCHEMA: core (si no existe) +-- ===================== +CREATE SCHEMA IF NOT EXISTS core; + +-- ===================== +-- TABLA: countries +-- Paises ISO 3166-1 +-- ===================== +CREATE TABLE IF NOT EXISTS core.countries ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + code VARCHAR(2) NOT NULL UNIQUE, -- ISO 3166-1 alpha-2 + code_alpha3 VARCHAR(3), -- ISO 3166-1 alpha-3 + name VARCHAR(255) NOT NULL, + phone_code VARCHAR(10), + currency_code VARCHAR(3), -- Default currency + + created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP +); + +CREATE INDEX IF NOT EXISTS idx_countries_code ON core.countries(code); +CREATE INDEX IF NOT EXISTS idx_countries_name ON core.countries(name); + +COMMENT ON TABLE core.countries IS 'Catalogo de paises ISO 3166-1'; + +-- ===================== +-- TABLA: states +-- Estados/Provincias/Regiones +-- ===================== +CREATE TABLE IF NOT EXISTS core.states ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + country_id UUID NOT NULL REFERENCES core.countries(id) ON DELETE CASCADE, + code VARCHAR(10) NOT NULL, -- Codigo del estado (ej: JAL, NL, QRO) + name VARCHAR(255) NOT NULL, + + -- Datos adicionales + timezone VARCHAR(50), -- Zona horaria predominante + is_active BOOLEAN DEFAULT TRUE, + + created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, + + UNIQUE(country_id, code) +); + +CREATE INDEX IF NOT EXISTS idx_states_country ON core.states(country_id); +CREATE INDEX IF NOT EXISTS idx_states_code ON core.states(code); +CREATE INDEX IF NOT EXISTS idx_states_active ON core.states(is_active) WHERE is_active = TRUE; + +COMMENT ON TABLE core.states IS 'Catalogo de estados/provincias/regiones por pais'; + +-- ===================== +-- TABLA: currencies +-- Monedas ISO 4217 +-- ===================== +CREATE TABLE IF NOT EXISTS 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 DEFAULT TRUE, + + created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP +); + +CREATE INDEX IF NOT EXISTS idx_currencies_code ON core.currencies(code); +CREATE INDEX IF NOT EXISTS idx_currencies_active ON core.currencies(active) WHERE active = TRUE; + +COMMENT ON TABLE core.currencies IS 'Catalogo de monedas ISO 4217'; + +-- ===================== +-- TABLA: currency_rates +-- Tipos de cambio historicos +-- ===================== +CREATE TABLE IF NOT EXISTS core.currency_rates ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID REFERENCES auth.tenants(id) ON DELETE CASCADE, -- NULL = global + + from_currency_id UUID NOT NULL REFERENCES core.currencies(id), + to_currency_id UUID NOT NULL REFERENCES core.currencies(id), + + rate DECIMAL(18, 8) NOT NULL, -- Tipo de cambio + rate_date DATE NOT NULL, -- Fecha del tipo de cambio + + -- Fuente del tipo de cambio + source VARCHAR(50) DEFAULT 'manual', -- manual, banxico, xe, openexchange + + created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, + created_by UUID REFERENCES auth.users(id), + + UNIQUE(tenant_id, from_currency_id, to_currency_id, rate_date) +); + +CREATE INDEX IF NOT EXISTS idx_currency_rates_tenant ON core.currency_rates(tenant_id); +CREATE INDEX IF NOT EXISTS idx_currency_rates_from ON core.currency_rates(from_currency_id); +CREATE INDEX IF NOT EXISTS idx_currency_rates_to ON core.currency_rates(to_currency_id); +CREATE INDEX IF NOT EXISTS idx_currency_rates_date ON core.currency_rates(rate_date DESC); +CREATE INDEX IF NOT EXISTS idx_currency_rates_lookup ON core.currency_rates(from_currency_id, to_currency_id, rate_date DESC); + +COMMENT ON TABLE core.currency_rates IS 'Historico de tipos de cambio entre monedas'; + +-- ===================== +-- TABLA: uom_categories +-- Categorias de unidades de medida +-- ===================== +CREATE TABLE IF NOT EXISTS core.uom_categories ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID REFERENCES auth.tenants(id) ON DELETE CASCADE, -- NULL = global + + name VARCHAR(100) NOT NULL, + description TEXT, + + created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, + + UNIQUE(tenant_id, name) +); + +CREATE INDEX IF NOT EXISTS idx_uom_categories_tenant ON core.uom_categories(tenant_id); + +COMMENT ON TABLE core.uom_categories IS 'Categorias de unidades de medida (peso, volumen, longitud, etc.)'; + +-- ===================== +-- TABLA: uom +-- Unidades de medida +-- ===================== +CREATE TABLE IF NOT EXISTS core.uom ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID REFERENCES auth.tenants(id) ON DELETE CASCADE, -- NULL = global + category_id UUID NOT NULL REFERENCES core.uom_categories(id) ON DELETE CASCADE, + + name VARCHAR(100) NOT NULL, + symbol VARCHAR(20) NOT NULL, + + -- Tipo: reference = unidad base de la categoria + uom_type VARCHAR(20) NOT NULL DEFAULT 'reference', -- reference, bigger, smaller + + -- Factor de conversion respecto a la unidad de referencia de la categoria + factor DECIMAL(18, 8) NOT NULL DEFAULT 1, + rounding DECIMAL(12, 6) DEFAULT 0.01, + + is_active BOOLEAN DEFAULT TRUE, + + created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, + + UNIQUE(tenant_id, category_id, name) +); + +CREATE INDEX IF NOT EXISTS idx_uom_tenant ON core.uom(tenant_id); +CREATE INDEX IF NOT EXISTS idx_uom_category ON core.uom(category_id); +CREATE INDEX IF NOT EXISTS idx_uom_active ON core.uom(is_active) WHERE is_active = TRUE; + +COMMENT ON TABLE core.uom IS 'Unidades de medida con factores de conversion'; + +-- ===================== +-- TABLA: product_categories +-- Categorias jerarquicas de productos +-- ===================== +CREATE TABLE IF NOT EXISTS core.product_categories ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, + parent_id UUID REFERENCES core.product_categories(id) ON DELETE SET NULL, + + code VARCHAR(50), + name VARCHAR(255) NOT NULL, + description TEXT, + + -- Jerarquia + hierarchy_path TEXT, + hierarchy_level INTEGER DEFAULT 0, + + -- Configuracion + is_active BOOLEAN DEFAULT TRUE, + + created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, + deleted_at TIMESTAMPTZ, + + UNIQUE(tenant_id, code) +); + +CREATE INDEX IF NOT EXISTS idx_product_categories_tenant ON core.product_categories(tenant_id); +CREATE INDEX IF NOT EXISTS idx_product_categories_parent ON core.product_categories(parent_id); +CREATE INDEX IF NOT EXISTS idx_product_categories_path ON core.product_categories(hierarchy_path); +CREATE INDEX IF NOT EXISTS idx_product_categories_active ON core.product_categories(is_active) WHERE is_active = TRUE; + +COMMENT ON TABLE core.product_categories IS 'Categorias jerarquicas de productos'; + +-- ===================== +-- TABLA: sequences +-- Secuencias para numeracion automatica +-- ===================== +CREATE TABLE IF NOT EXISTS core.sequences ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, + + code VARCHAR(50) NOT NULL, + name VARCHAR(255) NOT NULL, + description TEXT, + + -- Formato + prefix VARCHAR(20), + suffix VARCHAR(20), + padding INTEGER DEFAULT 5, + + -- Contador + next_number BIGINT DEFAULT 1, + + -- Reset + reset_period VARCHAR(20) DEFAULT 'never', -- never, daily, monthly, yearly + last_reset_at TIMESTAMPTZ, + + is_active BOOLEAN DEFAULT TRUE, + + created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, + + UNIQUE(tenant_id, code) +); + +CREATE INDEX IF NOT EXISTS idx_sequences_tenant ON core.sequences(tenant_id); +CREATE INDEX IF NOT EXISTS idx_sequences_code ON core.sequences(code); + +COMMENT ON TABLE core.sequences IS 'Secuencias para numeracion automatica de documentos'; + +-- ===================== +-- TABLA: payment_terms +-- Condiciones de pago +-- ===================== +CREATE TABLE IF NOT EXISTS core.payment_terms ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, + + code VARCHAR(50) NOT NULL, + name VARCHAR(255) NOT NULL, + description TEXT, + + -- Configuracion + is_immediate BOOLEAN DEFAULT FALSE, -- Pago inmediato/contado + + is_active BOOLEAN DEFAULT TRUE, + + created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, + deleted_at TIMESTAMPTZ, + + UNIQUE(tenant_id, code) +); + +CREATE INDEX IF NOT EXISTS idx_payment_terms_tenant ON core.payment_terms(tenant_id); +CREATE INDEX IF NOT EXISTS idx_payment_terms_code ON core.payment_terms(code); + +-- ===================== +-- TABLA: payment_term_lines +-- Lineas de condiciones de pago +-- ===================== +CREATE TABLE IF NOT EXISTS core.payment_term_lines ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + payment_term_id UUID NOT NULL REFERENCES core.payment_terms(id) ON DELETE CASCADE, + + sequence INTEGER DEFAULT 0, + + line_type VARCHAR(20) NOT NULL DEFAULT 'balance', -- percent, fixed, balance + value_percent DECIMAL(5, 2), -- Para type=percent + value_fixed DECIMAL(12, 2), -- Para type=fixed + + days INTEGER DEFAULT 0, -- Dias para vencimiento + day_of_month INTEGER, -- Dia especifico del mes (1-31) + + created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP +); + +CREATE INDEX IF NOT EXISTS idx_payment_term_lines_term ON core.payment_term_lines(payment_term_id); + +COMMENT ON TABLE core.payment_term_lines IS 'Lineas que componen una condicion de pago'; + +-- ===================== +-- TABLA: discount_rules +-- Reglas de descuento +-- ===================== +CREATE TABLE IF NOT EXISTS core.discount_rules ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, + + code VARCHAR(50) NOT NULL, + name VARCHAR(255) NOT NULL, + description TEXT, + + -- Tipo de descuento + discount_type VARCHAR(20) NOT NULL DEFAULT 'percent', -- percent, fixed + value DECIMAL(12, 2) NOT NULL, + + -- A que aplica + applies_to VARCHAR(30) DEFAULT 'all', -- all, product, category, customer, order + + -- Condiciones (JSONB para flexibilidad) + conditions JSONB DEFAULT '{}', + -- Ejemplo: {"min_qty": 10, "min_amount": 1000, "product_ids": [...]} + + -- Vigencia + valid_from TIMESTAMPTZ, + valid_until TIMESTAMPTZ, + + -- Prioridad (para resolver conflictos) + priority INTEGER DEFAULT 0, + + is_active BOOLEAN DEFAULT TRUE, + + created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, + deleted_at TIMESTAMPTZ, + + UNIQUE(tenant_id, code) +); + +CREATE INDEX IF NOT EXISTS idx_discount_rules_tenant ON core.discount_rules(tenant_id); +CREATE INDEX IF NOT EXISTS idx_discount_rules_code ON core.discount_rules(code); +CREATE INDEX IF NOT EXISTS idx_discount_rules_active ON core.discount_rules(is_active) WHERE is_active = TRUE; +CREATE INDEX IF NOT EXISTS idx_discount_rules_validity ON core.discount_rules(valid_from, valid_until); + +COMMENT ON TABLE core.discount_rules IS 'Reglas de descuento configurables'; + +-- ===================== +-- RLS POLICIES +-- ===================== + +-- Currency rates: tenant isolation (NULL tenant = global, available to all) +ALTER TABLE core.currency_rates ENABLE ROW LEVEL SECURITY; +CREATE POLICY tenant_isolation_currency_rates ON core.currency_rates + USING ( + tenant_id IS NULL OR + tenant_id = current_setting('app.current_tenant_id', true)::uuid + ); + +-- UoM Categories: tenant isolation +ALTER TABLE core.uom_categories ENABLE ROW LEVEL SECURITY; +CREATE POLICY tenant_isolation_uom_categories ON core.uom_categories + USING ( + tenant_id IS NULL OR + tenant_id = current_setting('app.current_tenant_id', true)::uuid + ); + +-- UoM: tenant isolation +ALTER TABLE core.uom ENABLE ROW LEVEL SECURITY; +CREATE POLICY tenant_isolation_uom ON core.uom + USING ( + tenant_id IS NULL OR + tenant_id = current_setting('app.current_tenant_id', true)::uuid + ); + +-- Product Categories: tenant isolation +ALTER TABLE core.product_categories ENABLE ROW LEVEL SECURITY; +CREATE POLICY tenant_isolation_product_categories ON core.product_categories + USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid); + +-- Sequences: tenant isolation +ALTER TABLE core.sequences ENABLE ROW LEVEL SECURITY; +CREATE POLICY tenant_isolation_sequences ON core.sequences + USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid); + +-- Payment Terms: tenant isolation +ALTER TABLE core.payment_terms ENABLE ROW LEVEL SECURITY; +CREATE POLICY tenant_isolation_payment_terms ON core.payment_terms + USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid); + +-- Discount Rules: tenant isolation +ALTER TABLE core.discount_rules ENABLE ROW LEVEL SECURITY; +CREATE POLICY tenant_isolation_discount_rules ON core.discount_rules + USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid); + +-- ===================== +-- FUNCIONES DE UTILIDAD +-- ===================== + +-- Funcion para obtener tipo de cambio mas reciente +CREATE OR REPLACE FUNCTION core.get_currency_rate( + p_from_currency VARCHAR(3), + p_to_currency VARCHAR(3), + p_date DATE DEFAULT CURRENT_DATE, + p_tenant_id UUID DEFAULT NULL +) +RETURNS DECIMAL(18, 8) AS $$ +DECLARE + v_rate DECIMAL(18, 8); +BEGIN + -- Si son la misma moneda, retornar 1 + IF p_from_currency = p_to_currency THEN + RETURN 1.0; + END IF; + + -- Buscar tipo de cambio directo (mas reciente hasta la fecha dada) + SELECT cr.rate INTO v_rate + FROM core.currency_rates cr + JOIN core.currencies fc ON fc.id = cr.from_currency_id AND fc.code = p_from_currency + JOIN core.currencies tc ON tc.id = cr.to_currency_id AND tc.code = p_to_currency + WHERE cr.rate_date <= p_date + AND (cr.tenant_id IS NULL OR cr.tenant_id = p_tenant_id) + ORDER BY cr.rate_date DESC, cr.tenant_id DESC NULLS LAST + LIMIT 1; + + IF v_rate IS NOT NULL THEN + RETURN v_rate; + END IF; + + -- Buscar tipo de cambio inverso + SELECT 1.0 / cr.rate INTO v_rate + FROM core.currency_rates cr + JOIN core.currencies fc ON fc.id = cr.from_currency_id AND fc.code = p_to_currency + JOIN core.currencies tc ON tc.id = cr.to_currency_id AND tc.code = p_from_currency + WHERE cr.rate_date <= p_date + AND (cr.tenant_id IS NULL OR cr.tenant_id = p_tenant_id) + ORDER BY cr.rate_date DESC, cr.tenant_id DESC NULLS LAST + LIMIT 1; + + RETURN v_rate; -- NULL si no se encuentra +END; +$$ LANGUAGE plpgsql; + +-- Funcion para convertir cantidad entre UoM +CREATE OR REPLACE FUNCTION core.convert_uom( + p_quantity DECIMAL, + p_from_uom_id UUID, + p_to_uom_id UUID +) +RETURNS DECIMAL(18, 8) AS $$ +DECLARE + v_from_factor DECIMAL(18, 8); + v_to_factor DECIMAL(18, 8); + v_from_category UUID; + v_to_category UUID; +BEGIN + -- Si son la misma UoM, retornar la cantidad + IF p_from_uom_id = p_to_uom_id THEN + RETURN p_quantity; + END IF; + + -- Obtener factores y categorias + SELECT factor, category_id INTO v_from_factor, v_from_category + FROM core.uom WHERE id = p_from_uom_id; + + SELECT factor, category_id INTO v_to_factor, v_to_category + FROM core.uom WHERE id = p_to_uom_id; + + -- Validar que sean de la misma categoria + IF v_from_category != v_to_category THEN + RAISE EXCEPTION 'Cannot convert between different UoM categories'; + END IF; + + -- Convertir: primero a unidad de referencia, luego a destino + RETURN p_quantity * v_from_factor / v_to_factor; +END; +$$ LANGUAGE plpgsql; + +-- ===================== +-- FIN DEL ARCHIVO +-- ===================== diff --git a/seeds/dev/04-seed-catalogs.sql b/seeds/dev/04-seed-catalogs.sql new file mode 100644 index 0000000..c411c87 --- /dev/null +++ b/seeds/dev/04-seed-catalogs.sql @@ -0,0 +1,259 @@ +-- ============================================================= +-- ARCHIVO: 04-seed-catalogs.sql +-- DESCRIPCION: Datos semilla para catalogos maestros +-- VERSION: 1.0.0 +-- PROYECTO: ERP-Core V2 +-- FECHA: 2026-01-18 +-- ============================================================= + +-- ===================== +-- PAISES (ISO 3166-1) +-- ===================== + +INSERT INTO core.countries (code, code_alpha3, name, phone_code, currency_code) VALUES +-- America del Norte +('MX', 'MEX', 'México', '+52', 'MXN'), +('US', 'USA', 'Estados Unidos', '+1', 'USD'), +('CA', 'CAN', 'Canadá', '+1', 'CAD'), +-- America Central +('GT', 'GTM', 'Guatemala', '+502', 'GTQ'), +('BZ', 'BLZ', 'Belice', '+501', 'BZD'), +('SV', 'SLV', 'El Salvador', '+503', 'USD'), +('HN', 'HND', 'Honduras', '+504', 'HNL'), +('NI', 'NIC', 'Nicaragua', '+505', 'NIO'), +('CR', 'CRI', 'Costa Rica', '+506', 'CRC'), +('PA', 'PAN', 'Panamá', '+507', 'PAB'), +-- America del Sur +('CO', 'COL', 'Colombia', '+57', 'COP'), +('VE', 'VEN', 'Venezuela', '+58', 'VES'), +('EC', 'ECU', 'Ecuador', '+593', 'USD'), +('PE', 'PER', 'Perú', '+51', 'PEN'), +('BO', 'BOL', 'Bolivia', '+591', 'BOB'), +('CL', 'CHL', 'Chile', '+56', 'CLP'), +('AR', 'ARG', 'Argentina', '+54', 'ARS'), +('UY', 'URY', 'Uruguay', '+598', 'UYU'), +('PY', 'PRY', 'Paraguay', '+595', 'PYG'), +('BR', 'BRA', 'Brasil', '+55', 'BRL'), +-- Europa +('ES', 'ESP', 'España', '+34', 'EUR'), +('DE', 'DEU', 'Alemania', '+49', 'EUR'), +('FR', 'FRA', 'Francia', '+33', 'EUR'), +('IT', 'ITA', 'Italia', '+39', 'EUR'), +('GB', 'GBR', 'Reino Unido', '+44', 'GBP'), +('PT', 'PRT', 'Portugal', '+351', 'EUR'), +('NL', 'NLD', 'Países Bajos', '+31', 'EUR'), +('BE', 'BEL', 'Bélgica', '+32', 'EUR'), +('CH', 'CHE', 'Suiza', '+41', 'CHF'), +-- Asia +('CN', 'CHN', 'China', '+86', 'CNY'), +('JP', 'JPN', 'Japón', '+81', 'JPY'), +('KR', 'KOR', 'Corea del Sur', '+82', 'KRW'), +('IN', 'IND', 'India', '+91', 'INR') +ON CONFLICT (code) DO NOTHING; + +-- ===================== +-- ESTADOS DE MEXICO +-- ===================== + +-- Obtener ID de México +DO $$ +DECLARE + mexico_id UUID; +BEGIN + SELECT id INTO mexico_id FROM core.countries WHERE code = 'MX'; + + IF mexico_id IS NOT NULL THEN + INSERT INTO core.states (country_id, code, name, timezone, is_active) VALUES + (mexico_id, 'AGU', 'Aguascalientes', 'America/Mexico_City', true), + (mexico_id, 'BCN', 'Baja California', 'America/Tijuana', true), + (mexico_id, 'BCS', 'Baja California Sur', 'America/Mazatlan', true), + (mexico_id, 'CAM', 'Campeche', 'America/Merida', true), + (mexico_id, 'CHP', 'Chiapas', 'America/Mexico_City', true), + (mexico_id, 'CHH', 'Chihuahua', 'America/Chihuahua', true), + (mexico_id, 'CMX', 'Ciudad de México', 'America/Mexico_City', true), + (mexico_id, 'COA', 'Coahuila', 'America/Monterrey', true), + (mexico_id, 'COL', 'Colima', 'America/Mexico_City', true), + (mexico_id, 'DUR', 'Durango', 'America/Monterrey', true), + (mexico_id, 'GUA', 'Guanajuato', 'America/Mexico_City', true), + (mexico_id, 'GRO', 'Guerrero', 'America/Mexico_City', true), + (mexico_id, 'HID', 'Hidalgo', 'America/Mexico_City', true), + (mexico_id, 'JAL', 'Jalisco', 'America/Mexico_City', true), + (mexico_id, 'MEX', 'Estado de México', 'America/Mexico_City', true), + (mexico_id, 'MIC', 'Michoacán', 'America/Mexico_City', true), + (mexico_id, 'MOR', 'Morelos', 'America/Mexico_City', true), + (mexico_id, 'NAY', 'Nayarit', 'America/Mazatlan', true), + (mexico_id, 'NLE', 'Nuevo León', 'America/Monterrey', true), + (mexico_id, 'OAX', 'Oaxaca', 'America/Mexico_City', true), + (mexico_id, 'PUE', 'Puebla', 'America/Mexico_City', true), + (mexico_id, 'QUE', 'Querétaro', 'America/Mexico_City', true), + (mexico_id, 'ROO', 'Quintana Roo', 'America/Cancun', true), + (mexico_id, 'SLP', 'San Luis Potosí', 'America/Mexico_City', true), + (mexico_id, 'SIN', 'Sinaloa', 'America/Mazatlan', true), + (mexico_id, 'SON', 'Sonora', 'America/Hermosillo', true), + (mexico_id, 'TAB', 'Tabasco', 'America/Mexico_City', true), + (mexico_id, 'TAM', 'Tamaulipas', 'America/Monterrey', true), + (mexico_id, 'TLA', 'Tlaxcala', 'America/Mexico_City', true), + (mexico_id, 'VER', 'Veracruz', 'America/Mexico_City', true), + (mexico_id, 'YUC', 'Yucatán', 'America/Merida', true), + (mexico_id, 'ZAC', 'Zacatecas', 'America/Mexico_City', true) + ON CONFLICT (country_id, code) DO NOTHING; + END IF; +END $$; + +-- ===================== +-- MONEDAS (ISO 4217) +-- ===================== + +INSERT INTO core.currencies (code, name, symbol, decimals, rounding, active) VALUES +-- Americas +('MXN', 'Peso Mexicano', '$', 2, 0.01, true), +('USD', 'Dólar Estadounidense', '$', 2, 0.01, true), +('CAD', 'Dólar Canadiense', '$', 2, 0.01, true), +('BRL', 'Real Brasileño', 'R$', 2, 0.01, true), +('ARS', 'Peso Argentino', '$', 2, 0.01, true), +('CLP', 'Peso Chileno', '$', 0, 1, true), +('COP', 'Peso Colombiano', '$', 0, 1, true), +('PEN', 'Sol Peruano', 'S/', 2, 0.01, true), +('GTQ', 'Quetzal', 'Q', 2, 0.01, true), +-- Europa +('EUR', 'Euro', '€', 2, 0.01, true), +('GBP', 'Libra Esterlina', '£', 2, 0.01, true), +('CHF', 'Franco Suizo', 'Fr', 2, 0.01, true), +-- Asia +('JPY', 'Yen Japonés', '¥', 0, 1, true), +('CNY', 'Yuan Chino', '¥', 2, 0.01, true), +('KRW', 'Won Surcoreano', '₩', 0, 1, true), +('INR', 'Rupia India', '₹', 2, 0.01, true) +ON CONFLICT (code) DO NOTHING; + +-- ===================== +-- TIPOS DE CAMBIO INICIALES (MXN base) +-- ===================== + +DO $$ +DECLARE + mxn_id UUID; + usd_id UUID; + eur_id UUID; + cad_id UUID; +BEGIN + SELECT id INTO mxn_id FROM core.currencies WHERE code = 'MXN'; + SELECT id INTO usd_id FROM core.currencies WHERE code = 'USD'; + SELECT id INTO eur_id FROM core.currencies WHERE code = 'EUR'; + SELECT id INTO cad_id FROM core.currencies WHERE code = 'CAD'; + + IF mxn_id IS NOT NULL AND usd_id IS NOT NULL THEN + INSERT INTO core.currency_rates (tenant_id, from_currency_id, to_currency_id, rate, rate_date, source) + VALUES + (NULL, usd_id, mxn_id, 17.50, CURRENT_DATE, 'manual'), + (NULL, eur_id, mxn_id, 19.20, CURRENT_DATE, 'manual'), + (NULL, cad_id, mxn_id, 13.10, CURRENT_DATE, 'manual') + ON CONFLICT DO NOTHING; + END IF; +END $$; + +-- ===================== +-- CATEGORIAS DE UOM (Globales) +-- ===================== + +INSERT INTO core.uom_categories (tenant_id, name, description) VALUES +(NULL, 'Unidad', 'Unidades contables'), +(NULL, 'Peso', 'Unidades de peso/masa'), +(NULL, 'Longitud', 'Unidades de longitud'), +(NULL, 'Volumen', 'Unidades de volumen'), +(NULL, 'Área', 'Unidades de área'), +(NULL, 'Tiempo', 'Unidades de tiempo') +ON CONFLICT (tenant_id, name) DO NOTHING; + +-- ===================== +-- UOM (Globales) +-- ===================== + +DO $$ +DECLARE + cat_unit UUID; + cat_peso UUID; + cat_longitud UUID; + cat_volumen UUID; + cat_area UUID; + cat_tiempo UUID; +BEGIN + SELECT id INTO cat_unit FROM core.uom_categories WHERE name = 'Unidad' AND tenant_id IS NULL; + SELECT id INTO cat_peso FROM core.uom_categories WHERE name = 'Peso' AND tenant_id IS NULL; + SELECT id INTO cat_longitud FROM core.uom_categories WHERE name = 'Longitud' AND tenant_id IS NULL; + SELECT id INTO cat_volumen FROM core.uom_categories WHERE name = 'Volumen' AND tenant_id IS NULL; + SELECT id INTO cat_area FROM core.uom_categories WHERE name = 'Área' AND tenant_id IS NULL; + SELECT id INTO cat_tiempo FROM core.uom_categories WHERE name = 'Tiempo' AND tenant_id IS NULL; + + -- Unidad + IF cat_unit IS NOT NULL THEN + INSERT INTO core.uom (tenant_id, category_id, name, symbol, uom_type, factor, is_active) VALUES + (NULL, cat_unit, 'Pieza', 'pz', 'reference', 1, true), + (NULL, cat_unit, 'Docena', 'doc', 'bigger', 12, true), + (NULL, cat_unit, 'Ciento', 'cto', 'bigger', 100, true), + (NULL, cat_unit, 'Millar', 'mil', 'bigger', 1000, true), + (NULL, cat_unit, 'Par', 'par', 'bigger', 2, true) + ON CONFLICT (tenant_id, category_id, name) DO NOTHING; + END IF; + + -- Peso + IF cat_peso IS NOT NULL THEN + INSERT INTO core.uom (tenant_id, category_id, name, symbol, uom_type, factor, is_active) VALUES + (NULL, cat_peso, 'Kilogramo', 'kg', 'reference', 1, true), + (NULL, cat_peso, 'Gramo', 'g', 'smaller', 0.001, true), + (NULL, cat_peso, 'Miligramo', 'mg', 'smaller', 0.000001, true), + (NULL, cat_peso, 'Tonelada', 't', 'bigger', 1000, true), + (NULL, cat_peso, 'Libra', 'lb', 'smaller', 0.453592, true), + (NULL, cat_peso, 'Onza', 'oz', 'smaller', 0.0283495, true) + ON CONFLICT (tenant_id, category_id, name) DO NOTHING; + END IF; + + -- Longitud + IF cat_longitud IS NOT NULL THEN + INSERT INTO core.uom (tenant_id, category_id, name, symbol, uom_type, factor, is_active) VALUES + (NULL, cat_longitud, 'Metro', 'm', 'reference', 1, true), + (NULL, cat_longitud, 'Centímetro', 'cm', 'smaller', 0.01, true), + (NULL, cat_longitud, 'Milímetro', 'mm', 'smaller', 0.001, true), + (NULL, cat_longitud, 'Kilómetro', 'km', 'bigger', 1000, true), + (NULL, cat_longitud, 'Pulgada', 'in', 'smaller', 0.0254, true), + (NULL, cat_longitud, 'Pie', 'ft', 'smaller', 0.3048, true), + (NULL, cat_longitud, 'Yarda', 'yd', 'smaller', 0.9144, true) + ON CONFLICT (tenant_id, category_id, name) DO NOTHING; + END IF; + + -- Volumen + IF cat_volumen IS NOT NULL THEN + INSERT INTO core.uom (tenant_id, category_id, name, symbol, uom_type, factor, is_active) VALUES + (NULL, cat_volumen, 'Litro', 'L', 'reference', 1, true), + (NULL, cat_volumen, 'Mililitro', 'mL', 'smaller', 0.001, true), + (NULL, cat_volumen, 'Metro cúbico', 'm³', 'bigger', 1000, true), + (NULL, cat_volumen, 'Galón US', 'gal', 'bigger', 3.78541, true), + (NULL, cat_volumen, 'Onza líquida', 'fl oz', 'smaller', 0.0295735, true) + ON CONFLICT (tenant_id, category_id, name) DO NOTHING; + END IF; + + -- Área + IF cat_area IS NOT NULL THEN + INSERT INTO core.uom (tenant_id, category_id, name, symbol, uom_type, factor, is_active) VALUES + (NULL, cat_area, 'Metro cuadrado', 'm²', 'reference', 1, true), + (NULL, cat_area, 'Centímetro cuadrado', 'cm²', 'smaller', 0.0001, true), + (NULL, cat_area, 'Kilómetro cuadrado', 'km²', 'bigger', 1000000, true), + (NULL, cat_area, 'Hectárea', 'ha', 'bigger', 10000, true), + (NULL, cat_area, 'Pie cuadrado', 'ft²', 'smaller', 0.092903, true) + ON CONFLICT (tenant_id, category_id, name) DO NOTHING; + END IF; + + -- Tiempo + IF cat_tiempo IS NOT NULL THEN + INSERT INTO core.uom (tenant_id, category_id, name, symbol, uom_type, factor, is_active) VALUES + (NULL, cat_tiempo, 'Hora', 'h', 'reference', 1, true), + (NULL, cat_tiempo, 'Minuto', 'min', 'smaller', 0.0166667, true), + (NULL, cat_tiempo, 'Día', 'd', 'bigger', 24, true), + (NULL, cat_tiempo, 'Semana', 'sem', 'bigger', 168, true) + ON CONFLICT (tenant_id, category_id, name) DO NOTHING; + END IF; +END $$; + +-- ===================== +-- FIN DEL ARCHIVO +-- =====================