DDL (20-core-catalogs.sql): - Countries, States tables with ISO codes - Currencies with ISO 4217 - Currency rates with historical tracking - UoM categories and units with conversion factors - Product categories hierarchy - Sequences, Payment terms, Discount rules Seed data (04-seed-catalogs.sql): - 33 countries (Americas, Europe, Asia) - 32 Mexican states with timezones - 16 currencies (MXN, USD, EUR, etc.) - Initial exchange rates - 6 UoM categories with 30+ units Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
468 lines
16 KiB
PL/PgSQL
468 lines
16 KiB
PL/PgSQL
-- =============================================================
|
|
-- 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
|
|
-- =====================
|