Compare commits

...

No commits in common. "57f41859de2761d1b13de7c0aa95c963695c813e" and "1cd8996d192869ea2cfb4b861f457c3ccd1f7686" have entirely different histories.

15 changed files with 26 additions and 2926 deletions

23
.gitignore vendored Normal file
View File

@ -0,0 +1,23 @@
# Environment
.env
.env.local
.env.*
# Backups
*.bak
*.backup
*.dump
# Logs
*.log
# IDE
.idea/
.vscode/
*.swp
*.swo
.DS_Store
# Temp
tmp/
temp/

3
README.md Normal file
View File

@ -0,0 +1,3 @@
# erp-core-database-v2
Database de erp-core - Workspace V2

View File

@ -1,467 +0,0 @@
-- =============================================================
-- 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
-- =====================

View File

@ -1,295 +0,0 @@
-- =============================================================
-- ARCHIVO: 21-fiscal-catalogs.sql
-- DESCRIPCION: Catalogos fiscales - SAT Mexico, regimenes, CFDI
-- VERSION: 1.0.0
-- PROYECTO: ERP-Core V2
-- FECHA: 2026-01-18
-- =============================================================
-- =====================
-- SCHEMA: fiscal
-- =====================
CREATE SCHEMA IF NOT EXISTS fiscal;
-- =====================
-- TABLA: tax_categories
-- Categorias de impuestos (IVA, ISR, IEPS, etc.)
-- =====================
CREATE TABLE IF NOT EXISTS fiscal.tax_categories (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
code VARCHAR(20) NOT NULL UNIQUE, -- IVA, ISR, IEPS, etc.
name VARCHAR(100) NOT NULL,
description TEXT,
-- Tipo de impuesto
tax_nature VARCHAR(20) NOT NULL DEFAULT 'tax', -- tax, withholding, both
-- Configuracion SAT Mexico
sat_code VARCHAR(10), -- Codigo SAT (002=IVA, 001=ISR, etc.)
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_tax_categories_code ON fiscal.tax_categories(code);
CREATE INDEX IF NOT EXISTS idx_tax_categories_sat ON fiscal.tax_categories(sat_code);
CREATE INDEX IF NOT EXISTS idx_tax_categories_active ON fiscal.tax_categories(is_active) WHERE is_active = TRUE;
COMMENT ON TABLE fiscal.tax_categories IS 'Categorias de impuestos (IVA, ISR, IEPS, etc.)';
-- =====================
-- TABLA: fiscal_regimes
-- Regimenes fiscales SAT Mexico (Catalogo c_RegimenFiscal)
-- =====================
CREATE TABLE IF NOT EXISTS fiscal.fiscal_regimes (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
code VARCHAR(10) NOT NULL UNIQUE, -- Codigo SAT (601, 603, 612, etc.)
name VARCHAR(255) NOT NULL,
description TEXT,
-- Aplica a persona fisica o moral
applies_to VARCHAR(20) NOT NULL DEFAULT 'both', -- natural (fisica), legal (moral), both
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_fiscal_regimes_code ON fiscal.fiscal_regimes(code);
CREATE INDEX IF NOT EXISTS idx_fiscal_regimes_applies ON fiscal.fiscal_regimes(applies_to);
CREATE INDEX IF NOT EXISTS idx_fiscal_regimes_active ON fiscal.fiscal_regimes(is_active) WHERE is_active = TRUE;
COMMENT ON TABLE fiscal.fiscal_regimes IS 'Catalogo de regimenes fiscales SAT (c_RegimenFiscal)';
-- =====================
-- TABLA: cfdi_uses
-- Uso del CFDI SAT Mexico (Catalogo c_UsoCFDI)
-- =====================
CREATE TABLE IF NOT EXISTS fiscal.cfdi_uses (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
code VARCHAR(10) NOT NULL UNIQUE, -- Codigo SAT (G01, G02, G03, etc.)
name VARCHAR(255) NOT NULL,
description TEXT,
-- Aplica a persona fisica o moral
applies_to VARCHAR(20) NOT NULL DEFAULT 'both', -- natural, legal, both
-- Regimenes fiscales permitidos (NULL = todos)
allowed_regimes VARCHAR[] DEFAULT NULL,
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_cfdi_uses_code ON fiscal.cfdi_uses(code);
CREATE INDEX IF NOT EXISTS idx_cfdi_uses_applies ON fiscal.cfdi_uses(applies_to);
CREATE INDEX IF NOT EXISTS idx_cfdi_uses_active ON fiscal.cfdi_uses(is_active) WHERE is_active = TRUE;
COMMENT ON TABLE fiscal.cfdi_uses IS 'Catalogo de uso del CFDI SAT (c_UsoCFDI)';
-- =====================
-- TABLA: payment_methods
-- Formas de pago SAT Mexico (Catalogo c_FormaPago)
-- =====================
CREATE TABLE IF NOT EXISTS fiscal.payment_methods (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
code VARCHAR(10) NOT NULL UNIQUE, -- Codigo SAT (01, 02, 03, etc.)
name VARCHAR(100) NOT NULL,
description TEXT,
-- Configuracion
requires_bank_info BOOLEAN DEFAULT FALSE, -- Requiere info bancaria
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_payment_methods_code ON fiscal.payment_methods(code);
CREATE INDEX IF NOT EXISTS idx_payment_methods_active ON fiscal.payment_methods(is_active) WHERE is_active = TRUE;
COMMENT ON TABLE fiscal.payment_methods IS 'Catalogo de formas de pago SAT (c_FormaPago)';
-- =====================
-- TABLA: payment_types
-- Metodos de pago SAT Mexico (Catalogo c_MetodoPago)
-- =====================
CREATE TABLE IF NOT EXISTS fiscal.payment_types (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
code VARCHAR(10) NOT NULL UNIQUE, -- PUE, PPD
name VARCHAR(100) NOT NULL,
description TEXT,
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_payment_types_code ON fiscal.payment_types(code);
CREATE INDEX IF NOT EXISTS idx_payment_types_active ON fiscal.payment_types(is_active) WHERE is_active = TRUE;
COMMENT ON TABLE fiscal.payment_types IS 'Catalogo de metodos de pago SAT (c_MetodoPago)';
-- =====================
-- TABLA: withholding_types
-- Tipos de retencion
-- =====================
CREATE TABLE IF NOT EXISTS fiscal.withholding_types (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
code VARCHAR(20) NOT NULL UNIQUE,
name VARCHAR(100) NOT NULL,
description TEXT,
-- Tasa de retencion por defecto
default_rate DECIMAL(5, 2) NOT NULL DEFAULT 0,
-- Categoria de impuesto asociada
tax_category_id UUID REFERENCES fiscal.tax_categories(id),
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_withholding_types_code ON fiscal.withholding_types(code);
CREATE INDEX IF NOT EXISTS idx_withholding_types_category ON fiscal.withholding_types(tax_category_id);
CREATE INDEX IF NOT EXISTS idx_withholding_types_active ON fiscal.withholding_types(is_active) WHERE is_active = TRUE;
COMMENT ON TABLE fiscal.withholding_types IS 'Tipos de retencion fiscal';
-- =====================
-- DATOS INICIALES: tax_categories
-- =====================
INSERT INTO fiscal.tax_categories (code, name, description, tax_nature, sat_code) VALUES
('IVA', 'Impuesto al Valor Agregado', 'Impuesto al consumo aplicado a bienes y servicios', 'tax', '002'),
('ISR', 'Impuesto Sobre la Renta', 'Impuesto a los ingresos', 'withholding', '001'),
('IEPS', 'Impuesto Especial sobre Produccion y Servicios', 'Impuesto a productos especiales', 'tax', '003'),
('IVA_RET', 'IVA Retenido', 'Retencion de IVA', 'withholding', '002'),
('ISR_RET', 'ISR Retenido', 'Retencion de ISR', 'withholding', '001'),
('CEDULAR', 'Impuesto Cedular', 'Impuesto local sobre ingresos', 'tax', NULL),
('ISH', 'Impuesto Sobre Hospedaje', 'Impuesto estatal al hospedaje', 'tax', NULL)
ON CONFLICT (code) DO NOTHING;
-- =====================
-- DATOS INICIALES: fiscal_regimes (SAT Mexico c_RegimenFiscal)
-- =====================
INSERT INTO fiscal.fiscal_regimes (code, name, applies_to) VALUES
('601', 'General de Ley Personas Morales', 'legal'),
('603', 'Personas Morales con Fines no Lucrativos', 'legal'),
('605', 'Sueldos y Salarios e Ingresos Asimilados a Salarios', 'natural'),
('606', 'Arrendamiento', 'natural'),
('607', 'Régimen de Enajenación o Adquisición de Bienes', 'natural'),
('608', 'Demás ingresos', 'natural'),
('609', 'Consolidación', 'legal'),
('610', 'Residentes en el Extranjero sin Establecimiento Permanente en México', 'both'),
('611', 'Ingresos por Dividendos (socios y accionistas)', 'natural'),
('612', 'Personas Físicas con Actividades Empresariales y Profesionales', 'natural'),
('614', 'Ingresos por intereses', 'natural'),
('615', 'Régimen de los ingresos por obtención de premios', 'natural'),
('616', 'Sin obligaciones fiscales', 'both'),
('620', 'Sociedades Cooperativas de Producción que optan por diferir sus ingresos', 'legal'),
('621', 'Incorporación Fiscal', 'natural'),
('622', 'Actividades Agrícolas, Ganaderas, Silvícolas y Pesqueras', 'both'),
('623', 'Opcional para Grupos de Sociedades', 'legal'),
('624', 'Coordinados', 'legal'),
('625', 'Régimen de las Actividades Empresariales con ingresos a través de Plataformas Tecnológicas', 'natural'),
('626', 'Régimen Simplificado de Confianza', 'both')
ON CONFLICT (code) DO NOTHING;
-- =====================
-- DATOS INICIALES: cfdi_uses (SAT Mexico c_UsoCFDI)
-- =====================
INSERT INTO fiscal.cfdi_uses (code, name, applies_to) VALUES
('G01', 'Adquisición de mercancías', 'both'),
('G02', 'Devoluciones, descuentos o bonificaciones', 'both'),
('G03', 'Gastos en general', 'both'),
('I01', 'Construcciones', 'both'),
('I02', 'Mobiliario y equipo de oficina por inversiones', 'both'),
('I03', 'Equipo de transporte', 'both'),
('I04', 'Equipo de cómputo y accesorios', 'both'),
('I05', 'Dados, troqueles, moldes, matrices y herramental', 'both'),
('I06', 'Comunicaciones telefónicas', 'both'),
('I07', 'Comunicaciones satelitales', 'both'),
('I08', 'Otra maquinaria y equipo', 'both'),
('D01', 'Honorarios médicos, dentales y gastos hospitalarios', 'natural'),
('D02', 'Gastos médicos por incapacidad o discapacidad', 'natural'),
('D03', 'Gastos funerales', 'natural'),
('D04', 'Donativos', 'natural'),
('D05', 'Intereses reales efectivamente pagados por créditos hipotecarios (casa habitación)', 'natural'),
('D06', 'Aportaciones voluntarias al SAR', 'natural'),
('D07', 'Primas por seguros de gastos médicos', 'natural'),
('D08', 'Gastos de transportación escolar obligatoria', 'natural'),
('D09', 'Depósitos en cuentas para el ahorro, primas que tengan como base planes de pensiones', 'natural'),
('D10', 'Pagos por servicios educativos (colegiaturas)', 'natural'),
('S01', 'Sin efectos fiscales', 'both'),
('CP01', 'Pagos', 'both'),
('CN01', 'Nómina', 'natural')
ON CONFLICT (code) DO NOTHING;
-- =====================
-- DATOS INICIALES: payment_methods (SAT Mexico c_FormaPago)
-- =====================
INSERT INTO fiscal.payment_methods (code, name, requires_bank_info) VALUES
('01', 'Efectivo', false),
('02', 'Cheque nominativo', true),
('03', 'Transferencia electrónica de fondos', true),
('04', 'Tarjeta de crédito', true),
('05', 'Monedero electrónico', false),
('06', 'Dinero electrónico', false),
('08', 'Vales de despensa', false),
('12', 'Dación en pago', false),
('13', 'Pago por subrogación', false),
('14', 'Pago por consignación', false),
('15', 'Condonación', false),
('17', 'Compensación', false),
('23', 'Novación', false),
('24', 'Confusión', false),
('25', 'Remisión de deuda', false),
('26', 'Prescripción o caducidad', false),
('27', 'A satisfacción del acreedor', false),
('28', 'Tarjeta de débito', true),
('29', 'Tarjeta de servicios', true),
('30', 'Aplicación de anticipos', false),
('31', 'Intermediario pagos', false),
('99', 'Por definir', false)
ON CONFLICT (code) DO NOTHING;
-- =====================
-- DATOS INICIALES: payment_types (SAT Mexico c_MetodoPago)
-- =====================
INSERT INTO fiscal.payment_types (code, name, description) VALUES
('PUE', 'Pago en una sola exhibición', 'El pago se realiza en una sola exhibición al momento de emitir el CFDI'),
('PPD', 'Pago en parcialidades o diferido', 'El pago se realiza en parcialidades o de forma diferida')
ON CONFLICT (code) DO NOTHING;
-- =====================
-- DATOS INICIALES: withholding_types
-- =====================
INSERT INTO fiscal.withholding_types (code, name, description, default_rate, tax_category_id) VALUES
('ISR_10', 'Retención ISR 10%', 'Retención de ISR al 10% para servicios profesionales', 10.00,
(SELECT id FROM fiscal.tax_categories WHERE code = 'ISR')),
('ISR_10.67', 'Retención ISR 10.67%', 'Retención de ISR al 10.67% para arrendamiento', 10.67,
(SELECT id FROM fiscal.tax_categories WHERE code = 'ISR')),
('IVA_RET_10.67', 'Retención IVA 10.67%', 'Retención de IVA para servicios profesionales persona moral', 10.67,
(SELECT id FROM fiscal.tax_categories WHERE code = 'IVA_RET')),
('IVA_RET_4', 'Retención IVA 4%', 'Retención de IVA al 4% para autotransporte', 4.00,
(SELECT id FROM fiscal.tax_categories WHERE code = 'IVA_RET'))
ON CONFLICT (code) DO NOTHING;
-- =====================
-- FIN DEL ARCHIVO
-- =====================

View File

@ -1,140 +0,0 @@
-- =============================================================
-- ARCHIVO: 46-purchases-matching.sql
-- DESCRIPCION: 3-Way Matching (PO-Receipt-Invoice)
-- VERSION: 1.0.0
-- PROYECTO: ERP-Core V2
-- FECHA: 2026-01-20
-- DEPENDE DE: 23-purchases.sql
-- =============================================================
-- =====================
-- TABLA: purchase_order_matching
-- Registro de matching por orden de compra
-- =====================
CREATE TABLE IF NOT EXISTS purchases.purchase_order_matching (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE,
purchase_order_id UUID NOT NULL REFERENCES purchases.purchase_orders(id) ON DELETE RESTRICT,
-- Estado del matching
status VARCHAR(20) NOT NULL DEFAULT 'pending', -- pending, partial_receipt, received, partial_invoice, matched, mismatch
-- Totales
total_ordered DECIMAL(15, 2) NOT NULL,
total_received DECIMAL(15, 2) DEFAULT 0,
total_invoiced DECIMAL(15, 2) DEFAULT 0,
-- Varianzas calculadas
receipt_variance DECIMAL(15, 2) GENERATED ALWAYS AS (total_ordered - total_received) STORED,
invoice_variance DECIMAL(15, 2) GENERATED ALWAYS AS (total_received - total_invoiced) STORED,
-- Referencias al ultimo documento
last_receipt_id UUID REFERENCES purchases.purchase_receipts(id),
last_invoice_id UUID,
-- Matching completado
matched_at TIMESTAMPTZ,
matched_by UUID REFERENCES auth.users(id),
-- Metadata
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
UNIQUE(tenant_id, purchase_order_id)
);
-- Indices para purchase_order_matching
CREATE INDEX IF NOT EXISTS idx_purchase_order_matching_tenant ON purchases.purchase_order_matching(tenant_id);
CREATE INDEX IF NOT EXISTS idx_purchase_order_matching_po ON purchases.purchase_order_matching(purchase_order_id);
CREATE INDEX IF NOT EXISTS idx_purchase_order_matching_status ON purchases.purchase_order_matching(status);
CREATE INDEX IF NOT EXISTS idx_purchase_order_matching_receipt ON purchases.purchase_order_matching(last_receipt_id);
-- =====================
-- TABLA: purchase_matching_lines
-- Matching por linea de orden de compra
-- =====================
CREATE TABLE IF NOT EXISTS purchases.purchase_matching_lines (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
matching_id UUID NOT NULL REFERENCES purchases.purchase_order_matching(id) ON DELETE CASCADE,
order_item_id UUID NOT NULL REFERENCES purchases.purchase_order_items(id) ON DELETE RESTRICT,
tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE,
-- Cantidades
qty_ordered DECIMAL(15, 4) NOT NULL,
qty_received DECIMAL(15, 4) DEFAULT 0,
qty_invoiced DECIMAL(15, 4) DEFAULT 0,
-- Precios
price_ordered DECIMAL(15, 2) NOT NULL,
price_invoiced DECIMAL(15, 2) DEFAULT 0,
-- Varianzas calculadas
qty_variance DECIMAL(15, 4) GENERATED ALWAYS AS (qty_ordered - qty_received) STORED,
invoice_qty_variance DECIMAL(15, 4) GENERATED ALWAYS AS (qty_received - qty_invoiced) STORED,
price_variance DECIMAL(15, 2) GENERATED ALWAYS AS (price_ordered - price_invoiced) STORED,
-- Estado
status VARCHAR(20) DEFAULT 'pending', -- pending, partial, matched, mismatch
-- Metadata
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
);
-- Indices para purchase_matching_lines
CREATE INDEX IF NOT EXISTS idx_purchase_matching_lines_matching ON purchases.purchase_matching_lines(matching_id);
CREATE INDEX IF NOT EXISTS idx_purchase_matching_lines_order_item ON purchases.purchase_matching_lines(order_item_id);
CREATE INDEX IF NOT EXISTS idx_purchase_matching_lines_tenant ON purchases.purchase_matching_lines(tenant_id);
CREATE INDEX IF NOT EXISTS idx_purchase_matching_lines_status ON purchases.purchase_matching_lines(status);
-- =====================
-- TABLA: matching_exceptions
-- Excepciones de matching
-- =====================
CREATE TABLE IF NOT EXISTS purchases.matching_exceptions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE,
matching_id UUID REFERENCES purchases.purchase_order_matching(id) ON DELETE CASCADE,
matching_line_id UUID REFERENCES purchases.purchase_matching_lines(id) ON DELETE CASCADE,
-- Tipo de excepcion
exception_type VARCHAR(50) NOT NULL, -- over_receipt, short_receipt, over_invoice, short_invoice, price_variance
-- Valores
expected_value DECIMAL(15, 4),
actual_value DECIMAL(15, 4),
variance_value DECIMAL(15, 4),
variance_percent DECIMAL(5, 2),
-- Resolucion
status VARCHAR(20) DEFAULT 'pending', -- pending, approved, rejected
resolved_at TIMESTAMPTZ,
resolved_by UUID REFERENCES auth.users(id),
resolution_notes TEXT,
-- Metadata
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
);
-- Indices para matching_exceptions
CREATE INDEX IF NOT EXISTS idx_matching_exceptions_tenant ON purchases.matching_exceptions(tenant_id);
CREATE INDEX IF NOT EXISTS idx_matching_exceptions_matching ON purchases.matching_exceptions(matching_id);
CREATE INDEX IF NOT EXISTS idx_matching_exceptions_line ON purchases.matching_exceptions(matching_line_id);
CREATE INDEX IF NOT EXISTS idx_matching_exceptions_type ON purchases.matching_exceptions(exception_type);
CREATE INDEX IF NOT EXISTS idx_matching_exceptions_status ON purchases.matching_exceptions(status);
-- =====================
-- COMENTARIOS
-- =====================
COMMENT ON TABLE purchases.purchase_order_matching IS 'Registro de 3-way matching por orden de compra (PO-Receipt-Invoice)';
COMMENT ON COLUMN purchases.purchase_order_matching.status IS 'Estado: pending, partial_receipt, received, partial_invoice, matched, mismatch';
COMMENT ON COLUMN purchases.purchase_order_matching.receipt_variance IS 'Varianza = total_ordered - total_received (columna generada)';
COMMENT ON COLUMN purchases.purchase_order_matching.invoice_variance IS 'Varianza = total_received - total_invoiced (columna generada)';
COMMENT ON TABLE purchases.purchase_matching_lines IS 'Matching por linea de detalle de orden de compra';
COMMENT ON COLUMN purchases.purchase_matching_lines.qty_variance IS 'Varianza de cantidad = qty_ordered - qty_received (columna generada)';
COMMENT ON COLUMN purchases.purchase_matching_lines.invoice_qty_variance IS 'Varianza de factura = qty_received - qty_invoiced (columna generada)';
COMMENT ON COLUMN purchases.purchase_matching_lines.price_variance IS 'Varianza de precio = price_ordered - price_invoiced (columna generada)';
COMMENT ON TABLE purchases.matching_exceptions IS 'Excepciones detectadas durante el proceso de matching';
COMMENT ON COLUMN purchases.matching_exceptions.exception_type IS 'Tipo: over_receipt, short_receipt, over_invoice, short_invoice, price_variance';
COMMENT ON COLUMN purchases.matching_exceptions.status IS 'Estado: pending, approved, rejected';

View File

@ -1,154 +0,0 @@
-- =============================================================
-- ARCHIVO: 50-financial-schema.sql
-- DESCRIPCION: Schema de contabilidad financiera y tipos enumerados
-- VERSION: 1.0.0
-- PROYECTO: ERP-Core V2
-- FECHA: 2026-01-20
-- DEPENDE DE: Ninguno (define el schema base)
-- =============================================================
-- =====================
-- SCHEMA: financial
-- Schema para modulo de contabilidad general
-- =====================
CREATE SCHEMA IF NOT EXISTS financial;
-- =====================
-- EXTENSIONES REQUERIDAS
-- =====================
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
-- =====================
-- TIPOS ENUMERADOS
-- =====================
-- Tipo de cuenta contable (activo, pasivo, capital, ingreso, gasto)
DO $$ BEGIN
CREATE TYPE financial.account_type_enum AS ENUM (
'asset', -- Activo
'liability', -- Pasivo
'equity', -- Capital/Patrimonio
'income', -- Ingreso
'expense' -- Gasto
);
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
-- Tipo de diario contable
DO $$ BEGIN
CREATE TYPE financial.journal_type_enum AS ENUM (
'sale', -- Ventas
'purchase', -- Compras
'cash', -- Caja
'bank', -- Banco
'general' -- General
);
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
-- Estado de asiento contable
DO $$ BEGIN
CREATE TYPE financial.entry_status_enum AS ENUM (
'draft', -- Borrador
'posted', -- Publicado/Contabilizado
'cancelled' -- Cancelado
);
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
-- Estado de periodo fiscal
DO $$ BEGIN
CREATE TYPE financial.period_status_enum AS ENUM (
'open', -- Abierto
'closed' -- Cerrado
);
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
-- Tipo de factura
DO $$ BEGIN
CREATE TYPE financial.invoice_type_enum AS ENUM (
'customer', -- Factura de cliente (venta)
'supplier' -- Factura de proveedor (compra)
);
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
-- Estado de factura
DO $$ BEGIN
CREATE TYPE financial.invoice_status_enum AS ENUM (
'draft', -- Borrador
'open', -- Abierta/Validada
'paid', -- Pagada
'cancelled' -- Cancelada
);
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
-- Tipo de pago
DO $$ BEGIN
CREATE TYPE financial.payment_type_enum AS ENUM (
'inbound', -- Entrada (cobro)
'outbound' -- Salida (pago)
);
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
-- Metodo de pago
DO $$ BEGIN
CREATE TYPE financial.payment_method_enum AS ENUM (
'cash', -- Efectivo
'bank_transfer', -- Transferencia bancaria
'check', -- Cheque
'card', -- Tarjeta
'other' -- Otro
);
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
-- Estado de pago
DO $$ BEGIN
CREATE TYPE financial.payment_status_enum AS ENUM (
'draft', -- Borrador
'posted', -- Contabilizado
'reconciled', -- Conciliado
'cancelled' -- Cancelado
);
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
-- Tipo de impuesto
DO $$ BEGIN
CREATE TYPE financial.tax_type_enum AS ENUM (
'sales', -- Solo ventas
'purchase', -- Solo compras
'all' -- Ambos (ventas y compras)
);
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
-- =====================
-- COMENTARIOS DEL SCHEMA
-- =====================
COMMENT ON SCHEMA financial IS 'Schema para el modulo de contabilidad general: plan de cuentas, diarios, asientos, facturas financieras y pagos';
COMMENT ON TYPE financial.account_type_enum IS 'Clasificacion de cuentas contables: activo, pasivo, capital, ingreso, gasto';
COMMENT ON TYPE financial.journal_type_enum IS 'Tipo de diario contable: ventas, compras, caja, banco, general';
COMMENT ON TYPE financial.entry_status_enum IS 'Estado del asiento contable: borrador, publicado, cancelado';
COMMENT ON TYPE financial.period_status_enum IS 'Estado del periodo fiscal: abierto, cerrado';
COMMENT ON TYPE financial.invoice_type_enum IS 'Tipo de factura financiera: cliente (venta), proveedor (compra)';
COMMENT ON TYPE financial.invoice_status_enum IS 'Estado de factura: borrador, abierta, pagada, cancelada';
COMMENT ON TYPE financial.payment_type_enum IS 'Direccion del pago: inbound (cobro), outbound (pago)';
COMMENT ON TYPE financial.payment_method_enum IS 'Metodo de pago: efectivo, transferencia, cheque, tarjeta, otro';
COMMENT ON TYPE financial.payment_status_enum IS 'Estado del pago: borrador, contabilizado, conciliado, cancelado';
COMMENT ON TYPE financial.tax_type_enum IS 'Aplicacion del impuesto: ventas, compras, ambos';

View File

@ -1,143 +0,0 @@
-- =============================================================
-- ARCHIVO: 51-financial-accounts.sql
-- DESCRIPCION: Plan de cuentas, tipos de cuenta y mapeos
-- VERSION: 1.0.0
-- PROYECTO: ERP-Core V2
-- FECHA: 2026-01-20
-- DEPENDE DE: 50-financial-schema.sql
-- =============================================================
-- =====================
-- TABLA: account_types
-- Catalogo de tipos de cuenta contable
-- =====================
CREATE TABLE IF NOT EXISTS financial.account_types (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
-- Identificacion
code VARCHAR(20) NOT NULL UNIQUE,
name VARCHAR(100) NOT NULL,
-- Clasificacion
account_type financial.account_type_enum NOT NULL,
-- Descripcion
description TEXT,
-- Metadata
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
);
-- Indices para account_types
CREATE INDEX IF NOT EXISTS idx_financial_account_types_code ON financial.account_types(code);
CREATE INDEX IF NOT EXISTS idx_financial_account_types_type ON financial.account_types(account_type);
-- =====================
-- TABLA: accounts
-- Plan de cuentas contables
-- =====================
CREATE TABLE IF NOT EXISTS financial.accounts (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
-- Multi-tenant
tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE,
company_id UUID, -- FK a company si existe multi-company
-- Identificacion
code VARCHAR(50) NOT NULL,
name VARCHAR(255) NOT NULL,
-- Clasificacion
account_type_id UUID NOT NULL REFERENCES financial.account_types(id) ON DELETE RESTRICT,
-- Jerarquia (cuenta padre para estructura arborea)
parent_id UUID REFERENCES financial.accounts(id) ON DELETE SET NULL,
-- Moneda preferida
currency_id UUID, -- FK a catalogo de monedas si existe
-- Configuracion
is_reconcilable BOOLEAN DEFAULT FALSE, -- Permite conciliacion bancaria
is_deprecated BOOLEAN DEFAULT FALSE, -- Cuenta obsoleta (no usar en nuevos movimientos)
-- Notas
notes TEXT,
-- Audit columns
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
created_by UUID REFERENCES auth.users(id),
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
updated_by UUID REFERENCES auth.users(id),
deleted_at TIMESTAMPTZ,
-- Constraint de unicidad por tenant
UNIQUE(tenant_id, code)
);
-- Indices para accounts
CREATE INDEX IF NOT EXISTS idx_financial_accounts_tenant ON financial.accounts(tenant_id);
CREATE INDEX IF NOT EXISTS idx_financial_accounts_company ON financial.accounts(company_id);
CREATE INDEX IF NOT EXISTS idx_financial_accounts_code ON financial.accounts(code);
CREATE INDEX IF NOT EXISTS idx_financial_accounts_type ON financial.accounts(account_type_id);
CREATE INDEX IF NOT EXISTS idx_financial_accounts_parent ON financial.accounts(parent_id);
CREATE INDEX IF NOT EXISTS idx_financial_accounts_active ON financial.accounts(tenant_id) WHERE deleted_at IS NULL AND is_deprecated = FALSE;
CREATE INDEX IF NOT EXISTS idx_financial_accounts_reconcilable ON financial.accounts(tenant_id, is_reconcilable) WHERE is_reconcilable = TRUE;
-- =====================
-- TABLA: account_mappings
-- Mapeos de cuentas para automatizaciones
-- Ej: cuenta_ingreso_default, cuenta_iva_16, cuenta_banco_principal
-- =====================
CREATE TABLE IF NOT EXISTS financial.account_mappings (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
-- Multi-tenant
tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE,
company_id UUID,
-- Identificacion del mapeo
mapping_type VARCHAR(50) NOT NULL, -- Ej: default_income, default_expense, vat_16, bank_main
-- Cuenta mapeada
account_id UUID NOT NULL REFERENCES financial.accounts(id) ON DELETE CASCADE,
-- Descripcion
description TEXT,
-- Estado
is_active BOOLEAN DEFAULT TRUE,
-- Audit columns
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
created_by UUID REFERENCES auth.users(id),
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
updated_by UUID REFERENCES auth.users(id),
-- Un solo mapeo activo por tipo por tenant/company
UNIQUE(tenant_id, company_id, mapping_type)
);
-- Indices para account_mappings
CREATE INDEX IF NOT EXISTS idx_financial_account_mappings_tenant ON financial.account_mappings(tenant_id);
CREATE INDEX IF NOT EXISTS idx_financial_account_mappings_company ON financial.account_mappings(company_id);
CREATE INDEX IF NOT EXISTS idx_financial_account_mappings_type ON financial.account_mappings(mapping_type);
CREATE INDEX IF NOT EXISTS idx_financial_account_mappings_account ON financial.account_mappings(account_id);
CREATE INDEX IF NOT EXISTS idx_financial_account_mappings_active ON financial.account_mappings(tenant_id, is_active) WHERE is_active = TRUE;
-- =====================
-- COMENTARIOS
-- =====================
COMMENT ON TABLE financial.account_types IS 'Catalogo de tipos de cuenta contable (activo, pasivo, capital, ingreso, gasto)';
COMMENT ON COLUMN financial.account_types.code IS 'Codigo unico del tipo (ej: ASSET_CURRENT, LIABILITY_LONG)';
COMMENT ON COLUMN financial.account_types.account_type IS 'Clasificacion principal: asset, liability, equity, income, expense';
COMMENT ON TABLE financial.accounts IS 'Plan de cuentas contables con estructura jerarquica';
COMMENT ON COLUMN financial.accounts.code IS 'Codigo de cuenta (ej: 1100, 1100.01)';
COMMENT ON COLUMN financial.accounts.parent_id IS 'Referencia a cuenta padre para estructura de arbol';
COMMENT ON COLUMN financial.accounts.is_reconcilable IS 'TRUE si la cuenta permite conciliacion bancaria';
COMMENT ON COLUMN financial.accounts.is_deprecated IS 'TRUE si la cuenta esta obsoleta (no usar en nuevos movimientos)';
COMMENT ON TABLE financial.account_mappings IS 'Mapeos de cuentas para automatizaciones contables';
COMMENT ON COLUMN financial.account_mappings.mapping_type IS 'Tipo de mapeo (ej: default_income, vat_16, bank_main)';
COMMENT ON COLUMN financial.account_mappings.account_id IS 'Cuenta contable asociada al mapeo';

View File

@ -1,162 +0,0 @@
-- =============================================================
-- ARCHIVO: 52-financial-journals.sql
-- DESCRIPCION: Diarios contables y periodos fiscales
-- VERSION: 1.0.0
-- PROYECTO: ERP-Core V2
-- FECHA: 2026-01-20
-- DEPENDE DE: 50-financial-schema.sql, 51-financial-accounts.sql
-- =============================================================
-- =====================
-- TABLA: fiscal_years
-- Anos fiscales
-- =====================
CREATE TABLE IF NOT EXISTS financial.fiscal_years (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
-- Multi-tenant
tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE,
company_id UUID,
-- Identificacion
name VARCHAR(100) NOT NULL, -- Ej: "Ejercicio 2026"
code VARCHAR(20) NOT NULL, -- Ej: "FY2026"
-- Periodo
date_from DATE NOT NULL,
date_to DATE NOT NULL,
-- Estado
status financial.period_status_enum DEFAULT 'open',
-- Audit columns
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
created_by UUID REFERENCES auth.users(id),
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
updated_by UUID REFERENCES auth.users(id),
-- Validaciones
CONSTRAINT chk_fiscal_years_dates CHECK (date_to > date_from),
UNIQUE(tenant_id, code)
);
-- Indices para fiscal_years
CREATE INDEX IF NOT EXISTS idx_financial_fiscal_years_tenant ON financial.fiscal_years(tenant_id);
CREATE INDEX IF NOT EXISTS idx_financial_fiscal_years_code ON financial.fiscal_years(code);
CREATE INDEX IF NOT EXISTS idx_financial_fiscal_years_status ON financial.fiscal_years(status);
CREATE INDEX IF NOT EXISTS idx_financial_fiscal_years_dates ON financial.fiscal_years(date_from, date_to);
-- =====================
-- TABLA: fiscal_periods
-- Periodos fiscales (meses o trimestres dentro de un ano fiscal)
-- =====================
CREATE TABLE IF NOT EXISTS financial.fiscal_periods (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
-- Multi-tenant
tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE,
-- Relacion con ano fiscal
fiscal_year_id UUID NOT NULL REFERENCES financial.fiscal_years(id) ON DELETE CASCADE,
-- Identificacion
code VARCHAR(20) NOT NULL, -- Ej: "2026-01", "Q1-2026"
name VARCHAR(100) NOT NULL, -- Ej: "Enero 2026", "Primer Trimestre 2026"
-- Periodo
date_from DATE NOT NULL,
date_to DATE NOT NULL,
-- Estado
status financial.period_status_enum DEFAULT 'open',
-- Cierre
closed_at TIMESTAMPTZ,
closed_by UUID REFERENCES auth.users(id),
-- Audit columns
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
created_by UUID REFERENCES auth.users(id),
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
updated_by UUID REFERENCES auth.users(id),
-- Validaciones
CONSTRAINT chk_fiscal_periods_dates CHECK (date_to >= date_from),
UNIQUE(tenant_id, fiscal_year_id, code)
);
-- Indices para fiscal_periods
CREATE INDEX IF NOT EXISTS idx_financial_fiscal_periods_tenant ON financial.fiscal_periods(tenant_id);
CREATE INDEX IF NOT EXISTS idx_financial_fiscal_periods_year ON financial.fiscal_periods(fiscal_year_id);
CREATE INDEX IF NOT EXISTS idx_financial_fiscal_periods_code ON financial.fiscal_periods(code);
CREATE INDEX IF NOT EXISTS idx_financial_fiscal_periods_status ON financial.fiscal_periods(status);
CREATE INDEX IF NOT EXISTS idx_financial_fiscal_periods_dates ON financial.fiscal_periods(date_from, date_to);
CREATE INDEX IF NOT EXISTS idx_financial_fiscal_periods_open ON financial.fiscal_periods(tenant_id, status) WHERE status = 'open';
-- =====================
-- TABLA: journals
-- Diarios contables (ventas, compras, caja, banco, general)
-- =====================
CREATE TABLE IF NOT EXISTS financial.journals (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
-- Multi-tenant
tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE,
company_id UUID,
-- Identificacion
name VARCHAR(255) NOT NULL,
code VARCHAR(20) NOT NULL,
-- Tipo de diario
journal_type financial.journal_type_enum NOT NULL,
-- Cuenta por defecto (para asientos automaticos)
default_account_id UUID REFERENCES financial.accounts(id) ON DELETE SET NULL,
-- Secuencia para numeracion
sequence_id UUID, -- FK a sistema de secuencias si existe
-- Moneda preferida
currency_id UUID,
-- Estado
active BOOLEAN DEFAULT TRUE,
-- Audit columns con soft delete
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
created_by UUID REFERENCES auth.users(id),
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
updated_by UUID REFERENCES auth.users(id),
deleted_at TIMESTAMPTZ,
-- Unicidad por tenant
UNIQUE(tenant_id, code)
);
-- Indices para journals
CREATE INDEX IF NOT EXISTS idx_financial_journals_tenant ON financial.journals(tenant_id);
CREATE INDEX IF NOT EXISTS idx_financial_journals_company ON financial.journals(company_id);
CREATE INDEX IF NOT EXISTS idx_financial_journals_code ON financial.journals(code);
CREATE INDEX IF NOT EXISTS idx_financial_journals_type ON financial.journals(journal_type);
CREATE INDEX IF NOT EXISTS idx_financial_journals_default_account ON financial.journals(default_account_id);
CREATE INDEX IF NOT EXISTS idx_financial_journals_active ON financial.journals(tenant_id) WHERE active = TRUE AND deleted_at IS NULL;
CREATE INDEX IF NOT EXISTS idx_financial_journals_by_type_active ON financial.journals(tenant_id, journal_type) WHERE active = TRUE AND deleted_at IS NULL;
-- =====================
-- COMENTARIOS
-- =====================
COMMENT ON TABLE financial.fiscal_years IS 'Anos fiscales para organizacion contable';
COMMENT ON COLUMN financial.fiscal_years.code IS 'Codigo unico del ano fiscal (ej: FY2026)';
COMMENT ON COLUMN financial.fiscal_years.status IS 'Estado: open (permite movimientos), closed (no permite movimientos)';
COMMENT ON TABLE financial.fiscal_periods IS 'Periodos fiscales (meses o trimestres) dentro de un ano fiscal';
COMMENT ON COLUMN financial.fiscal_periods.code IS 'Codigo del periodo (ej: 2026-01, Q1-2026)';
COMMENT ON COLUMN financial.fiscal_periods.closed_at IS 'Fecha y hora de cierre del periodo';
COMMENT ON COLUMN financial.fiscal_periods.closed_by IS 'Usuario que cerro el periodo';
COMMENT ON TABLE financial.journals IS 'Diarios contables para agrupar asientos por tipo de operacion';
COMMENT ON COLUMN financial.journals.code IS 'Codigo unico del diario (ej: VTAS, COMP, CAJA, BCO)';
COMMENT ON COLUMN financial.journals.journal_type IS 'Tipo: sale (ventas), purchase (compras), cash (caja), bank (banco), general';
COMMENT ON COLUMN financial.journals.default_account_id IS 'Cuenta por defecto para asientos automaticos';
COMMENT ON COLUMN financial.journals.sequence_id IS 'Referencia a secuencia para numeracion automatica';

View File

@ -1,175 +0,0 @@
-- =============================================================
-- ARCHIVO: 53-financial-entries.sql
-- DESCRIPCION: Asientos contables y lineas de asiento
-- VERSION: 1.0.0
-- PROYECTO: ERP-Core V2
-- FECHA: 2026-01-20
-- DEPENDE DE: 50-financial-schema.sql, 51-financial-accounts.sql, 52-financial-journals.sql
-- =============================================================
-- =====================
-- TABLA: journal_entries
-- Asientos contables (cabecera)
-- =====================
CREATE TABLE IF NOT EXISTS financial.journal_entries (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
-- Multi-tenant
tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE,
company_id UUID,
-- Diario
journal_id UUID NOT NULL REFERENCES financial.journals(id) ON DELETE RESTRICT,
-- Identificacion
name VARCHAR(100) NOT NULL, -- Numero o identificador del asiento
ref VARCHAR(255), -- Referencia externa (factura, pago, etc.)
-- Fecha
date DATE NOT NULL,
-- Estado
status financial.entry_status_enum DEFAULT 'draft',
-- Notas
notes TEXT,
-- Periodo fiscal
fiscal_period_id UUID REFERENCES financial.fiscal_periods(id) ON DELETE RESTRICT,
-- Audit columns
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
created_by UUID REFERENCES auth.users(id),
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
updated_by UUID REFERENCES auth.users(id),
-- Publicacion
posted_at TIMESTAMPTZ,
posted_by UUID REFERENCES auth.users(id),
-- Cancelacion
cancelled_at TIMESTAMPTZ,
cancelled_by UUID REFERENCES auth.users(id)
);
-- Indices para journal_entries
CREATE INDEX IF NOT EXISTS idx_financial_journal_entries_tenant ON financial.journal_entries(tenant_id);
CREATE INDEX IF NOT EXISTS idx_financial_journal_entries_company ON financial.journal_entries(company_id);
CREATE INDEX IF NOT EXISTS idx_financial_journal_entries_journal ON financial.journal_entries(journal_id);
CREATE INDEX IF NOT EXISTS idx_financial_journal_entries_name ON financial.journal_entries(name);
CREATE INDEX IF NOT EXISTS idx_financial_journal_entries_ref ON financial.journal_entries(ref);
CREATE INDEX IF NOT EXISTS idx_financial_journal_entries_date ON financial.journal_entries(date);
CREATE INDEX IF NOT EXISTS idx_financial_journal_entries_status ON financial.journal_entries(status);
CREATE INDEX IF NOT EXISTS idx_financial_journal_entries_period ON financial.journal_entries(fiscal_period_id);
CREATE INDEX IF NOT EXISTS idx_financial_journal_entries_posted ON financial.journal_entries(tenant_id, status) WHERE status = 'posted';
CREATE INDEX IF NOT EXISTS idx_financial_journal_entries_draft ON financial.journal_entries(tenant_id, status) WHERE status = 'draft';
CREATE INDEX IF NOT EXISTS idx_financial_journal_entries_date_range ON financial.journal_entries(tenant_id, date, status);
-- =====================
-- TABLA: journal_entry_lines
-- Lineas de asiento contable (debe/haber)
-- =====================
CREATE TABLE IF NOT EXISTS financial.journal_entry_lines (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
-- Relacion con asiento (cascade delete)
entry_id UUID NOT NULL REFERENCES financial.journal_entries(id) ON DELETE CASCADE,
-- Multi-tenant (denormalizado para queries rapidas)
tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE,
-- Cuenta contable
account_id UUID NOT NULL REFERENCES financial.accounts(id) ON DELETE RESTRICT,
-- Partner asociado (opcional, para cuentas por cobrar/pagar)
partner_id UUID, -- FK a partners si existe
-- Montos (solo debe o solo haber, nunca ambos)
debit DECIMAL(15, 2) DEFAULT 0 CHECK (debit >= 0),
credit DECIMAL(15, 2) DEFAULT 0 CHECK (credit >= 0),
-- Descripcion de la linea
description TEXT,
-- Referencia adicional
ref VARCHAR(255),
-- Metadata
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
-- Validacion: debe tener debit XOR credit (no ambos, no ninguno)
CONSTRAINT chk_journal_entry_lines_debit_credit CHECK (
(debit > 0 AND credit = 0) OR (debit = 0 AND credit > 0)
)
);
-- Indices para journal_entry_lines
CREATE INDEX IF NOT EXISTS idx_financial_journal_entry_lines_entry ON financial.journal_entry_lines(entry_id);
CREATE INDEX IF NOT EXISTS idx_financial_journal_entry_lines_tenant ON financial.journal_entry_lines(tenant_id);
CREATE INDEX IF NOT EXISTS idx_financial_journal_entry_lines_account ON financial.journal_entry_lines(account_id);
CREATE INDEX IF NOT EXISTS idx_financial_journal_entry_lines_partner ON financial.journal_entry_lines(partner_id) WHERE partner_id IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_financial_journal_entry_lines_debit ON financial.journal_entry_lines(account_id, debit) WHERE debit > 0;
CREATE INDEX IF NOT EXISTS idx_financial_journal_entry_lines_credit ON financial.journal_entry_lines(account_id, credit) WHERE credit > 0;
-- =====================
-- FUNCION: Validar balance de asiento
-- Un asiento debe estar balanceado (sum debit = sum credit)
-- =====================
CREATE OR REPLACE FUNCTION financial.check_entry_balance()
RETURNS TRIGGER AS $$
DECLARE
v_total_debit DECIMAL(15, 2);
v_total_credit DECIMAL(15, 2);
v_entry_status financial.entry_status_enum;
BEGIN
-- Solo validar cuando el asiento se publica
SELECT status INTO v_entry_status
FROM financial.journal_entries
WHERE id = COALESCE(NEW.entry_id, OLD.entry_id);
-- Solo validar si el asiento esta siendo publicado
IF v_entry_status = 'posted' THEN
SELECT
COALESCE(SUM(debit), 0),
COALESCE(SUM(credit), 0)
INTO v_total_debit, v_total_credit
FROM financial.journal_entry_lines
WHERE entry_id = COALESCE(NEW.entry_id, OLD.entry_id);
IF v_total_debit != v_total_credit THEN
RAISE EXCEPTION 'El asiento no esta balanceado. Debe: %, Haber: %',
v_total_debit, v_total_credit;
END IF;
END IF;
RETURN COALESCE(NEW, OLD);
END;
$$ LANGUAGE plpgsql;
-- Trigger para validar balance (se ejecuta despues de INSERT/UPDATE/DELETE en lineas)
-- Nota: El trigger se crea pero puede deshabilitarse en ambientes de migracion
DROP TRIGGER IF EXISTS trg_check_entry_balance ON financial.journal_entry_lines;
-- CREATE TRIGGER trg_check_entry_balance
-- AFTER INSERT OR UPDATE OR DELETE ON financial.journal_entry_lines
-- FOR EACH ROW
-- EXECUTE FUNCTION financial.check_entry_balance();
-- =====================
-- COMENTARIOS
-- =====================
COMMENT ON TABLE financial.journal_entries IS 'Cabecera de asientos contables';
COMMENT ON COLUMN financial.journal_entries.name IS 'Numero o identificador unico del asiento';
COMMENT ON COLUMN financial.journal_entries.ref IS 'Referencia externa (numero de factura, pago, etc.)';
COMMENT ON COLUMN financial.journal_entries.status IS 'Estado: draft (editable), posted (contabilizado), cancelled';
COMMENT ON COLUMN financial.journal_entries.fiscal_period_id IS 'Periodo fiscal al que pertenece el asiento';
COMMENT ON COLUMN financial.journal_entries.posted_at IS 'Fecha y hora de publicacion/contabilizacion';
COMMENT ON COLUMN financial.journal_entries.cancelled_at IS 'Fecha y hora de cancelacion';
COMMENT ON TABLE financial.journal_entry_lines IS 'Lineas de asiento contable (partidas de debe y haber)';
COMMENT ON COLUMN financial.journal_entry_lines.account_id IS 'Cuenta contable afectada';
COMMENT ON COLUMN financial.journal_entry_lines.partner_id IS 'Partner asociado (para cuentas por cobrar/pagar)';
COMMENT ON COLUMN financial.journal_entry_lines.debit IS 'Monto al debe (cargo)';
COMMENT ON COLUMN financial.journal_entry_lines.credit IS 'Monto al haber (abono)';
COMMENT ON COLUMN financial.journal_entry_lines.description IS 'Descripcion o concepto de la linea';
COMMENT ON FUNCTION financial.check_entry_balance() IS 'Valida que el asiento este balanceado (sum debit = sum credit)';

View File

@ -1,167 +0,0 @@
-- =============================================================
-- ARCHIVO: 54-financial-invoices.sql
-- DESCRIPCION: Facturas contables (cliente/proveedor) y lineas de factura
-- VERSION: 1.0.0
-- PROYECTO: ERP-Core V2
-- FECHA: 2026-01-20
-- DEPENDE DE: 50-financial-schema.sql, 51-financial-accounts.sql, 52-financial-journals.sql, 53-financial-entries.sql
-- NOTA: Este modulo es para facturas desde perspectiva CONTABLE.
-- Para facturacion operativa ver 24-invoices.sql (schema billing)
-- =============================================================
-- =====================
-- TABLA: invoices
-- Facturas contables (cliente y proveedor)
-- =====================
CREATE TABLE IF NOT EXISTS financial.invoices (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
-- Multi-tenant
tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE,
company_id UUID,
-- Partner (cliente o proveedor)
partner_id UUID NOT NULL, -- FK a partners.partners
-- Tipo de factura
invoice_type financial.invoice_type_enum NOT NULL,
-- Identificacion
number VARCHAR(100) NOT NULL, -- Numero de factura
ref VARCHAR(255), -- Referencia externa
-- Fechas
invoice_date DATE NOT NULL,
due_date DATE,
-- Moneda
currency_id UUID, -- FK a catalogo de monedas
-- Montos
amount_untaxed DECIMAL(15, 2) DEFAULT 0, -- Subtotal sin impuestos
amount_tax DECIMAL(15, 2) DEFAULT 0, -- Total impuestos
amount_total DECIMAL(15, 2) DEFAULT 0, -- Total de la factura
amount_paid DECIMAL(15, 2) DEFAULT 0, -- Monto pagado
amount_residual DECIMAL(15, 2) GENERATED ALWAYS AS (amount_total - COALESCE(amount_paid, 0)) STORED, -- Saldo pendiente
-- Estado
status financial.invoice_status_enum DEFAULT 'draft',
-- Terminos de pago
payment_term_id UUID, -- FK a terminos de pago si existe
-- Relacion con contabilidad
journal_id UUID REFERENCES financial.journals(id) ON DELETE RESTRICT,
journal_entry_id UUID REFERENCES financial.journal_entries(id) ON DELETE SET NULL,
-- Notas
notes TEXT,
-- Audit columns
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
created_by UUID REFERENCES auth.users(id),
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
updated_by UUID REFERENCES auth.users(id),
-- Validacion
validated_at TIMESTAMPTZ,
validated_by UUID REFERENCES auth.users(id),
-- Cancelacion
cancelled_at TIMESTAMPTZ,
cancelled_by UUID REFERENCES auth.users(id),
-- Unicidad
UNIQUE(tenant_id, number, invoice_type)
);
-- Indices para invoices
CREATE INDEX IF NOT EXISTS idx_financial_invoices_tenant ON financial.invoices(tenant_id);
CREATE INDEX IF NOT EXISTS idx_financial_invoices_company ON financial.invoices(company_id);
CREATE INDEX IF NOT EXISTS idx_financial_invoices_partner ON financial.invoices(partner_id);
CREATE INDEX IF NOT EXISTS idx_financial_invoices_type ON financial.invoices(invoice_type);
CREATE INDEX IF NOT EXISTS idx_financial_invoices_number ON financial.invoices(number);
CREATE INDEX IF NOT EXISTS idx_financial_invoices_date ON financial.invoices(invoice_date);
CREATE INDEX IF NOT EXISTS idx_financial_invoices_due_date ON financial.invoices(due_date);
CREATE INDEX IF NOT EXISTS idx_financial_invoices_status ON financial.invoices(status);
CREATE INDEX IF NOT EXISTS idx_financial_invoices_journal ON financial.invoices(journal_id);
CREATE INDEX IF NOT EXISTS idx_financial_invoices_entry ON financial.invoices(journal_entry_id);
CREATE INDEX IF NOT EXISTS idx_financial_invoices_open ON financial.invoices(tenant_id, status) WHERE status = 'open';
CREATE INDEX IF NOT EXISTS idx_financial_invoices_unpaid ON financial.invoices(tenant_id, due_date) WHERE status = 'open' AND amount_paid < amount_total;
CREATE INDEX IF NOT EXISTS idx_financial_invoices_customer ON financial.invoices(tenant_id, partner_id, invoice_type) WHERE invoice_type = 'customer';
CREATE INDEX IF NOT EXISTS idx_financial_invoices_supplier ON financial.invoices(tenant_id, partner_id, invoice_type) WHERE invoice_type = 'supplier';
-- =====================
-- TABLA: invoice_lines
-- Lineas de factura contable
-- =====================
CREATE TABLE IF NOT EXISTS financial.invoice_lines (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
-- Relacion con factura (cascade delete)
invoice_id UUID NOT NULL REFERENCES financial.invoices(id) ON DELETE CASCADE,
-- Multi-tenant (denormalizado para queries rapidas)
tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE,
-- Producto (opcional)
product_id UUID, -- FK a products.products
-- Descripcion
description TEXT,
-- Cantidad y unidad
quantity DECIMAL(15, 4) NOT NULL DEFAULT 1,
uom_id UUID, -- FK a unidades de medida
-- Precio
price_unit DECIMAL(15, 2) NOT NULL DEFAULT 0,
-- Impuestos aplicables (array de UUIDs de taxes)
tax_ids UUID[] DEFAULT '{}',
-- Montos calculados
amount_untaxed DECIMAL(15, 2) DEFAULT 0, -- subtotal linea
amount_tax DECIMAL(15, 2) DEFAULT 0, -- impuestos linea
amount_total DECIMAL(15, 2) DEFAULT 0, -- total linea
-- Cuenta contable
account_id UUID REFERENCES financial.accounts(id) ON DELETE RESTRICT,
-- Audit columns
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
created_by UUID REFERENCES auth.users(id),
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
updated_by UUID REFERENCES auth.users(id)
);
-- Indices para invoice_lines
CREATE INDEX IF NOT EXISTS idx_financial_invoice_lines_invoice ON financial.invoice_lines(invoice_id);
CREATE INDEX IF NOT EXISTS idx_financial_invoice_lines_tenant ON financial.invoice_lines(tenant_id);
CREATE INDEX IF NOT EXISTS idx_financial_invoice_lines_product ON financial.invoice_lines(product_id) WHERE product_id IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_financial_invoice_lines_account ON financial.invoice_lines(account_id);
CREATE INDEX IF NOT EXISTS idx_financial_invoice_lines_tax_ids ON financial.invoice_lines USING GIN(tax_ids);
-- =====================
-- COMENTARIOS
-- =====================
COMMENT ON TABLE financial.invoices IS 'Facturas contables (perspectiva financiera)';
COMMENT ON COLUMN financial.invoices.invoice_type IS 'Tipo: customer (venta a cliente), supplier (compra a proveedor)';
COMMENT ON COLUMN financial.invoices.number IS 'Numero unico de factura';
COMMENT ON COLUMN financial.invoices.ref IS 'Referencia externa (numero de factura del proveedor, etc.)';
COMMENT ON COLUMN financial.invoices.amount_untaxed IS 'Subtotal sin impuestos';
COMMENT ON COLUMN financial.invoices.amount_tax IS 'Total de impuestos';
COMMENT ON COLUMN financial.invoices.amount_total IS 'Total de la factura (subtotal + impuestos)';
COMMENT ON COLUMN financial.invoices.amount_paid IS 'Monto pagado hasta el momento';
COMMENT ON COLUMN financial.invoices.amount_residual IS 'Saldo pendiente de pago (calculado)';
COMMENT ON COLUMN financial.invoices.status IS 'Estado: draft, open (validada), paid, cancelled';
COMMENT ON COLUMN financial.invoices.journal_entry_id IS 'Asiento contable generado al validar la factura';
COMMENT ON COLUMN financial.invoices.validated_at IS 'Fecha y hora de validacion/apertura';
COMMENT ON COLUMN financial.invoices.cancelled_at IS 'Fecha y hora de cancelacion';
COMMENT ON TABLE financial.invoice_lines IS 'Lineas de detalle de facturas contables';
COMMENT ON COLUMN financial.invoice_lines.product_id IS 'Producto asociado (opcional)';
COMMENT ON COLUMN financial.invoice_lines.quantity IS 'Cantidad facturada';
COMMENT ON COLUMN financial.invoice_lines.price_unit IS 'Precio unitario';
COMMENT ON COLUMN financial.invoice_lines.tax_ids IS 'Array de IDs de impuestos aplicables';
COMMENT ON COLUMN financial.invoice_lines.account_id IS 'Cuenta contable para el asiento';

View File

@ -1,174 +0,0 @@
-- =============================================================
-- ARCHIVO: 55-financial-payments.sql
-- DESCRIPCION: Pagos contables (cobros y pagos)
-- VERSION: 1.0.0
-- PROYECTO: ERP-Core V2
-- FECHA: 2026-01-20
-- DEPENDE DE: 50-financial-schema.sql, 51-financial-accounts.sql, 52-financial-journals.sql, 53-financial-entries.sql
-- NOTA: Este modulo es para pagos desde perspectiva CONTABLE.
-- Para pagos operativos ver 24-invoices.sql (schema billing)
-- =============================================================
-- =====================
-- TABLA: payments
-- Pagos contables (cobros entrantes y pagos salientes)
-- =====================
CREATE TABLE IF NOT EXISTS financial.payments (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
-- Multi-tenant
tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE,
company_id UUID,
-- Partner (cliente o proveedor)
partner_id UUID NOT NULL, -- FK a partners.partners
-- Tipo de pago
payment_type financial.payment_type_enum NOT NULL, -- inbound (cobro), outbound (pago)
-- Metodo de pago
payment_method financial.payment_method_enum NOT NULL,
-- Monto
amount DECIMAL(15, 2) NOT NULL CHECK (amount > 0),
-- Moneda
currency_id UUID, -- FK a catalogo de monedas
-- Fecha de pago
payment_date DATE NOT NULL,
-- Referencia
ref VARCHAR(255), -- Numero de cheque, referencia bancaria, etc.
-- Estado
status financial.payment_status_enum DEFAULT 'draft',
-- Relacion con contabilidad
journal_id UUID REFERENCES financial.journals(id) ON DELETE RESTRICT,
journal_entry_id UUID REFERENCES financial.journal_entries(id) ON DELETE SET NULL,
-- Notas
notes TEXT,
-- Audit columns
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
created_by UUID REFERENCES auth.users(id),
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
updated_by UUID REFERENCES auth.users(id),
-- Publicacion/Contabilizacion
posted_at TIMESTAMPTZ,
posted_by UUID REFERENCES auth.users(id)
);
-- Indices para payments
CREATE INDEX IF NOT EXISTS idx_financial_payments_tenant ON financial.payments(tenant_id);
CREATE INDEX IF NOT EXISTS idx_financial_payments_company ON financial.payments(company_id);
CREATE INDEX IF NOT EXISTS idx_financial_payments_partner ON financial.payments(partner_id);
CREATE INDEX IF NOT EXISTS idx_financial_payments_type ON financial.payments(payment_type);
CREATE INDEX IF NOT EXISTS idx_financial_payments_method ON financial.payments(payment_method);
CREATE INDEX IF NOT EXISTS idx_financial_payments_date ON financial.payments(payment_date);
CREATE INDEX IF NOT EXISTS idx_financial_payments_status ON financial.payments(status);
CREATE INDEX IF NOT EXISTS idx_financial_payments_journal ON financial.payments(journal_id);
CREATE INDEX IF NOT EXISTS idx_financial_payments_entry ON financial.payments(journal_entry_id);
CREATE INDEX IF NOT EXISTS idx_financial_payments_ref ON financial.payments(ref) WHERE ref IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_financial_payments_posted ON financial.payments(tenant_id, status) WHERE status = 'posted';
CREATE INDEX IF NOT EXISTS idx_financial_payments_inbound ON financial.payments(tenant_id, partner_id, payment_type) WHERE payment_type = 'inbound';
CREATE INDEX IF NOT EXISTS idx_financial_payments_outbound ON financial.payments(tenant_id, partner_id, payment_type) WHERE payment_type = 'outbound';
CREATE INDEX IF NOT EXISTS idx_financial_payments_date_range ON financial.payments(tenant_id, payment_date, status);
-- =====================
-- TABLA: payment_invoice_allocations
-- Aplicacion de pagos a facturas
-- =====================
CREATE TABLE IF NOT EXISTS financial.payment_invoice_allocations (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
-- Pago
payment_id UUID NOT NULL REFERENCES financial.payments(id) ON DELETE CASCADE,
-- Factura
invoice_id UUID NOT NULL REFERENCES financial.invoices(id) ON DELETE CASCADE,
-- Monto aplicado a esta factura
amount DECIMAL(15, 2) NOT NULL CHECK (amount > 0),
-- Fecha de aplicacion
allocation_date DATE NOT NULL DEFAULT CURRENT_DATE,
-- Audit columns
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
created_by UUID REFERENCES auth.users(id),
-- Un pago solo puede aplicarse una vez a cada factura
UNIQUE(payment_id, invoice_id)
);
-- Indices para payment_invoice_allocations
CREATE INDEX IF NOT EXISTS idx_financial_payment_allocations_payment ON financial.payment_invoice_allocations(payment_id);
CREATE INDEX IF NOT EXISTS idx_financial_payment_allocations_invoice ON financial.payment_invoice_allocations(invoice_id);
CREATE INDEX IF NOT EXISTS idx_financial_payment_allocations_date ON financial.payment_invoice_allocations(allocation_date);
-- =====================
-- FUNCION: Actualizar amount_paid en factura
-- =====================
CREATE OR REPLACE FUNCTION financial.update_invoice_amount_paid()
RETURNS TRIGGER AS $$
DECLARE
v_invoice_id UUID;
v_total_paid DECIMAL(15, 2);
BEGIN
-- Determinar la factura afectada
IF TG_OP = 'DELETE' THEN
v_invoice_id := OLD.invoice_id;
ELSE
v_invoice_id := NEW.invoice_id;
END IF;
-- Calcular total pagado para la factura
SELECT COALESCE(SUM(amount), 0)
INTO v_total_paid
FROM financial.payment_invoice_allocations
WHERE invoice_id = v_invoice_id;
-- Actualizar factura
UPDATE financial.invoices
SET
amount_paid = v_total_paid,
status = CASE
WHEN v_total_paid >= amount_total THEN 'paid'::financial.invoice_status_enum
WHEN v_total_paid > 0 THEN 'open'::financial.invoice_status_enum
ELSE status
END,
updated_at = CURRENT_TIMESTAMP
WHERE id = v_invoice_id;
RETURN COALESCE(NEW, OLD);
END;
$$ LANGUAGE plpgsql;
-- Trigger para actualizar amount_paid automaticamente
DROP TRIGGER IF EXISTS trg_update_invoice_amount_paid ON financial.payment_invoice_allocations;
CREATE TRIGGER trg_update_invoice_amount_paid
AFTER INSERT OR UPDATE OR DELETE ON financial.payment_invoice_allocations
FOR EACH ROW
EXECUTE FUNCTION financial.update_invoice_amount_paid();
-- =====================
-- COMENTARIOS
-- =====================
COMMENT ON TABLE financial.payments IS 'Pagos contables (cobros y pagos a proveedores)';
COMMENT ON COLUMN financial.payments.payment_type IS 'Tipo: inbound (cobro de cliente), outbound (pago a proveedor)';
COMMENT ON COLUMN financial.payments.payment_method IS 'Metodo: cash, bank_transfer, check, card, other';
COMMENT ON COLUMN financial.payments.amount IS 'Monto del pago (siempre positivo)';
COMMENT ON COLUMN financial.payments.ref IS 'Referencia: numero de cheque, referencia bancaria, etc.';
COMMENT ON COLUMN financial.payments.status IS 'Estado: draft, posted (contabilizado), reconciled, cancelled';
COMMENT ON COLUMN financial.payments.journal_entry_id IS 'Asiento contable generado al publicar el pago';
COMMENT ON COLUMN financial.payments.posted_at IS 'Fecha y hora de publicacion/contabilizacion';
COMMENT ON TABLE financial.payment_invoice_allocations IS 'Aplicacion de pagos a facturas especificas';
COMMENT ON COLUMN financial.payment_invoice_allocations.amount IS 'Monto del pago aplicado a esta factura';
COMMENT ON COLUMN financial.payment_invoice_allocations.allocation_date IS 'Fecha de aplicacion del pago';
COMMENT ON FUNCTION financial.update_invoice_amount_paid() IS 'Actualiza automaticamente amount_paid en facturas cuando se aplican pagos';

View File

@ -1,155 +0,0 @@
-- =============================================================
-- ARCHIVO: 56-financial-taxes.sql
-- DESCRIPCION: Impuestos contables (IVA, retenciones, etc.)
-- VERSION: 1.0.0
-- PROYECTO: ERP-Core V2
-- FECHA: 2026-01-20
-- DEPENDE DE: 50-financial-schema.sql, 51-financial-accounts.sql
-- =============================================================
-- =====================
-- TABLA: taxes
-- Catalogo de impuestos
-- =====================
CREATE TABLE IF NOT EXISTS financial.taxes (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
-- Multi-tenant
tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE,
company_id UUID,
-- Identificacion
name VARCHAR(100) NOT NULL, -- Ej: "IVA 16%", "Retencion ISR 10%"
code VARCHAR(20) NOT NULL, -- Ej: "IVA16", "RET_ISR10"
-- Tipo de impuesto
tax_type financial.tax_type_enum NOT NULL DEFAULT 'all',
-- Tasa
amount DECIMAL(5, 2) NOT NULL, -- Porcentaje (ej: 16.00 para 16%)
-- Configuracion
included_in_price BOOLEAN DEFAULT FALSE, -- TRUE si el precio ya incluye el impuesto
-- Estado
active BOOLEAN DEFAULT TRUE,
-- Cuentas contables asociadas (opcional)
account_id UUID REFERENCES financial.accounts(id) ON DELETE SET NULL, -- Cuenta de impuesto
refund_account_id UUID REFERENCES financial.accounts(id) ON DELETE SET NULL, -- Cuenta para devoluciones
-- Audit columns
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
created_by UUID REFERENCES auth.users(id),
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
updated_by UUID REFERENCES auth.users(id),
-- Unicidad por tenant
UNIQUE(tenant_id, code)
);
-- Indices para taxes
CREATE INDEX IF NOT EXISTS idx_financial_taxes_tenant ON financial.taxes(tenant_id);
CREATE INDEX IF NOT EXISTS idx_financial_taxes_company ON financial.taxes(company_id);
CREATE INDEX IF NOT EXISTS idx_financial_taxes_code ON financial.taxes(code);
CREATE INDEX IF NOT EXISTS idx_financial_taxes_type ON financial.taxes(tax_type);
CREATE INDEX IF NOT EXISTS idx_financial_taxes_active ON financial.taxes(tenant_id) WHERE active = TRUE;
CREATE INDEX IF NOT EXISTS idx_financial_taxes_sales ON financial.taxes(tenant_id, tax_type) WHERE tax_type IN ('sales', 'all') AND active = TRUE;
CREATE INDEX IF NOT EXISTS idx_financial_taxes_purchase ON financial.taxes(tenant_id, tax_type) WHERE tax_type IN ('purchase', 'all') AND active = TRUE;
CREATE INDEX IF NOT EXISTS idx_financial_taxes_account ON financial.taxes(account_id) WHERE account_id IS NOT NULL;
-- =====================
-- TABLA: tax_groups (opcional, para agrupar impuestos)
-- Grupos de impuestos para aplicacion conjunta
-- =====================
CREATE TABLE IF NOT EXISTS financial.tax_groups (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
-- Multi-tenant
tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE,
-- Identificacion
name VARCHAR(100) NOT NULL,
code VARCHAR(20) NOT NULL,
-- Descripcion
description TEXT,
-- Impuestos en el grupo (array de IDs)
tax_ids UUID[] DEFAULT '{}',
-- Estado
active BOOLEAN DEFAULT TRUE,
-- Audit columns
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
created_by UUID REFERENCES auth.users(id),
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
updated_by UUID REFERENCES auth.users(id),
UNIQUE(tenant_id, code)
);
-- Indices para tax_groups
CREATE INDEX IF NOT EXISTS idx_financial_tax_groups_tenant ON financial.tax_groups(tenant_id);
CREATE INDEX IF NOT EXISTS idx_financial_tax_groups_code ON financial.tax_groups(code);
CREATE INDEX IF NOT EXISTS idx_financial_tax_groups_active ON financial.tax_groups(tenant_id) WHERE active = TRUE;
CREATE INDEX IF NOT EXISTS idx_financial_tax_groups_tax_ids ON financial.tax_groups USING GIN(tax_ids);
-- =====================
-- DATOS SEMILLA: Impuestos comunes de Mexico
-- =====================
-- Nota: Estos se insertan condicionalmente. En produccion, los impuestos
-- se crean por tenant desde la aplicacion.
-- Funcion para insertar impuestos semilla
CREATE OR REPLACE FUNCTION financial.seed_default_taxes(p_tenant_id UUID)
RETURNS void AS $$
BEGIN
-- IVA 16% (tasa general)
INSERT INTO financial.taxes (tenant_id, code, name, tax_type, amount, included_in_price, active)
VALUES (p_tenant_id, 'IVA16', 'IVA 16%', 'all', 16.00, FALSE, TRUE)
ON CONFLICT (tenant_id, code) DO NOTHING;
-- IVA 8% (frontera)
INSERT INTO financial.taxes (tenant_id, code, name, tax_type, amount, included_in_price, active)
VALUES (p_tenant_id, 'IVA8', 'IVA 8% (Frontera)', 'all', 8.00, FALSE, TRUE)
ON CONFLICT (tenant_id, code) DO NOTHING;
-- IVA 0% (tasa cero)
INSERT INTO financial.taxes (tenant_id, code, name, tax_type, amount, included_in_price, active)
VALUES (p_tenant_id, 'IVA0', 'IVA 0%', 'all', 0.00, FALSE, TRUE)
ON CONFLICT (tenant_id, code) DO NOTHING;
-- IVA Exento
INSERT INTO financial.taxes (tenant_id, code, name, tax_type, amount, included_in_price, active)
VALUES (p_tenant_id, 'EXENTO', 'Exento de IVA', 'all', 0.00, FALSE, TRUE)
ON CONFLICT (tenant_id, code) DO NOTHING;
-- Retencion ISR 10%
INSERT INTO financial.taxes (tenant_id, code, name, tax_type, amount, included_in_price, active)
VALUES (p_tenant_id, 'RET_ISR10', 'Retencion ISR 10%', 'purchase', -10.00, FALSE, TRUE)
ON CONFLICT (tenant_id, code) DO NOTHING;
-- Retencion IVA 10.67%
INSERT INTO financial.taxes (tenant_id, code, name, tax_type, amount, included_in_price, active)
VALUES (p_tenant_id, 'RET_IVA', 'Retencion IVA 2/3', 'purchase', -10.67, FALSE, TRUE)
ON CONFLICT (tenant_id, code) DO NOTHING;
END;
$$ LANGUAGE plpgsql;
-- =====================
-- COMENTARIOS
-- =====================
COMMENT ON TABLE financial.taxes IS 'Catalogo de impuestos (IVA, retenciones, etc.)';
COMMENT ON COLUMN financial.taxes.code IS 'Codigo unico del impuesto (ej: IVA16, RET_ISR10)';
COMMENT ON COLUMN financial.taxes.tax_type IS 'Aplicacion: sales (solo ventas), purchase (solo compras), all (ambos)';
COMMENT ON COLUMN financial.taxes.amount IS 'Tasa del impuesto en porcentaje (ej: 16.00 para 16%). Negativo para retenciones.';
COMMENT ON COLUMN financial.taxes.included_in_price IS 'TRUE si el precio del producto ya incluye este impuesto';
COMMENT ON COLUMN financial.taxes.account_id IS 'Cuenta contable donde se registra el impuesto';
COMMENT ON COLUMN financial.taxes.refund_account_id IS 'Cuenta contable para devoluciones/notas de credito';
COMMENT ON TABLE financial.tax_groups IS 'Grupos de impuestos para aplicacion conjunta (ej: IVA + Retenciones)';
COMMENT ON COLUMN financial.tax_groups.tax_ids IS 'Array de IDs de impuestos que componen el grupo';
COMMENT ON FUNCTION financial.seed_default_taxes(UUID) IS 'Inserta impuestos predeterminados de Mexico para un tenant';

View File

@ -1,254 +0,0 @@
-- =============================================================
-- ARCHIVO: 57-financial-bank-reconciliation.sql
-- DESCRIPCION: Conciliacion bancaria - extractos, lineas y reglas de match
-- VERSION: 1.0.0
-- PROYECTO: ERP-Core V2
-- FECHA: 2026-01-20
-- DEPENDE DE: 50-financial-schema.sql, 51-financial-accounts.sql, 53-financial-entries.sql
-- =============================================================
-- =====================
-- TIPO ENUMERADO: Estado de extracto bancario
-- =====================
DO $$ BEGIN
CREATE TYPE financial.bank_statement_status_enum AS ENUM (
'draft', -- Borrador
'reconciling', -- En proceso de conciliacion
'reconciled' -- Conciliado
);
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
-- =====================
-- TIPO ENUMERADO: Tipo de regla de match
-- =====================
DO $$ BEGIN
CREATE TYPE financial.reconciliation_match_type_enum AS ENUM (
'exact_amount', -- Monto exacto
'reference_contains', -- Referencia contiene texto
'partner_name' -- Nombre de partner
);
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
-- =====================
-- TABLA: bank_statements
-- Extractos bancarios importados
-- =====================
CREATE TABLE IF NOT EXISTS financial.bank_statements (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
-- Multi-tenant
tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE,
company_id UUID,
-- Cuenta bancaria asociada (referencia a cuenta contable tipo banco)
bank_account_id UUID REFERENCES financial.accounts(id) ON DELETE RESTRICT,
-- Datos del extracto
statement_date DATE NOT NULL,
opening_balance DECIMAL(15, 2) NOT NULL DEFAULT 0,
closing_balance DECIMAL(15, 2) NOT NULL DEFAULT 0,
-- Estado
status financial.bank_statement_status_enum DEFAULT 'draft',
-- Importacion
imported_at TIMESTAMPTZ,
imported_by UUID REFERENCES auth.users(id),
-- Conciliacion
reconciled_at TIMESTAMPTZ,
reconciled_by UUID REFERENCES auth.users(id),
-- Audit columns
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
created_by UUID REFERENCES auth.users(id),
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
updated_by UUID REFERENCES auth.users(id)
);
-- Indices para bank_statements
CREATE INDEX IF NOT EXISTS idx_financial_bank_statements_tenant ON financial.bank_statements(tenant_id);
CREATE INDEX IF NOT EXISTS idx_financial_bank_statements_company ON financial.bank_statements(company_id);
CREATE INDEX IF NOT EXISTS idx_financial_bank_statements_bank_account ON financial.bank_statements(bank_account_id);
CREATE INDEX IF NOT EXISTS idx_financial_bank_statements_date ON financial.bank_statements(statement_date);
CREATE INDEX IF NOT EXISTS idx_financial_bank_statements_status ON financial.bank_statements(status);
CREATE INDEX IF NOT EXISTS idx_financial_bank_statements_tenant_date ON financial.bank_statements(tenant_id, statement_date DESC);
CREATE INDEX IF NOT EXISTS idx_financial_bank_statements_draft ON financial.bank_statements(tenant_id, status) WHERE status = 'draft';
CREATE INDEX IF NOT EXISTS idx_financial_bank_statements_reconciling ON financial.bank_statements(tenant_id, status) WHERE status = 'reconciling';
-- =====================
-- TABLA: bank_statement_lines
-- Lineas de extracto bancario (movimientos)
-- =====================
CREATE TABLE IF NOT EXISTS financial.bank_statement_lines (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
-- Relacion con extracto (cascade delete)
statement_id UUID NOT NULL REFERENCES financial.bank_statements(id) ON DELETE CASCADE,
-- Multi-tenant (denormalizado para queries rapidas)
tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE,
-- Datos del movimiento
transaction_date DATE NOT NULL,
value_date DATE,
description VARCHAR(500),
reference VARCHAR(100),
amount DECIMAL(15, 2) NOT NULL, -- Positivo = deposito, Negativo = retiro
-- Estado de conciliacion
is_reconciled BOOLEAN DEFAULT false,
reconciled_entry_id UUID REFERENCES financial.journal_entry_lines(id) ON DELETE SET NULL,
reconciled_at TIMESTAMPTZ,
reconciled_by UUID REFERENCES auth.users(id),
-- Partner detectado (automatico o manual)
partner_id UUID, -- FK a partners si existe
-- Notas adicionales
notes TEXT,
-- Audit columns
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
);
-- Indices para bank_statement_lines
CREATE INDEX IF NOT EXISTS idx_financial_bank_statement_lines_statement ON financial.bank_statement_lines(statement_id);
CREATE INDEX IF NOT EXISTS idx_financial_bank_statement_lines_tenant ON financial.bank_statement_lines(tenant_id);
CREATE INDEX IF NOT EXISTS idx_financial_bank_statement_lines_date ON financial.bank_statement_lines(transaction_date);
CREATE INDEX IF NOT EXISTS idx_financial_bank_statement_lines_reconciled ON financial.bank_statement_lines(is_reconciled);
CREATE INDEX IF NOT EXISTS idx_financial_bank_statement_lines_entry ON financial.bank_statement_lines(reconciled_entry_id) WHERE reconciled_entry_id IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_financial_bank_statement_lines_partner ON financial.bank_statement_lines(partner_id) WHERE partner_id IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_financial_bank_statement_lines_reference ON financial.bank_statement_lines(reference) WHERE reference IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_financial_bank_statement_lines_unreconciled ON financial.bank_statement_lines(tenant_id, statement_id) WHERE is_reconciled = false;
CREATE INDEX IF NOT EXISTS idx_financial_bank_statement_lines_amount ON financial.bank_statement_lines(amount);
-- =====================
-- TABLA: bank_reconciliation_rules
-- Reglas de conciliacion automatica
-- =====================
CREATE TABLE IF NOT EXISTS financial.bank_reconciliation_rules (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
-- Multi-tenant
tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE,
company_id UUID,
-- Identificacion de la regla
name VARCHAR(255) NOT NULL,
-- Tipo y valor del match
match_type financial.reconciliation_match_type_enum NOT NULL,
match_value VARCHAR(255) NOT NULL, -- Valor a buscar segun el tipo
-- Cuenta destino para auto-crear asiento
auto_account_id UUID REFERENCES financial.accounts(id) ON DELETE SET NULL,
-- Estado y prioridad
is_active BOOLEAN DEFAULT true,
priority INTEGER DEFAULT 0, -- Mayor prioridad = se evalua primero
-- Audit columns
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
created_by UUID REFERENCES auth.users(id),
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
updated_by UUID REFERENCES auth.users(id)
);
-- Indices para bank_reconciliation_rules
CREATE INDEX IF NOT EXISTS idx_financial_bank_reconciliation_rules_tenant ON financial.bank_reconciliation_rules(tenant_id);
CREATE INDEX IF NOT EXISTS idx_financial_bank_reconciliation_rules_company ON financial.bank_reconciliation_rules(company_id);
CREATE INDEX IF NOT EXISTS idx_financial_bank_reconciliation_rules_active ON financial.bank_reconciliation_rules(tenant_id, is_active) WHERE is_active = true;
CREATE INDEX IF NOT EXISTS idx_financial_bank_reconciliation_rules_priority ON financial.bank_reconciliation_rules(tenant_id, priority DESC);
CREATE INDEX IF NOT EXISTS idx_financial_bank_reconciliation_rules_match_type ON financial.bank_reconciliation_rules(match_type);
-- =====================
-- FUNCION: Calcular balance calculado del extracto
-- Verifica que opening_balance + sum(lines) = closing_balance
-- =====================
CREATE OR REPLACE FUNCTION financial.check_statement_balance(p_statement_id UUID)
RETURNS BOOLEAN AS $$
DECLARE
v_opening_balance DECIMAL(15, 2);
v_closing_balance DECIMAL(15, 2);
v_lines_total DECIMAL(15, 2);
v_calculated_closing DECIMAL(15, 2);
BEGIN
-- Obtener balances del extracto
SELECT opening_balance, closing_balance
INTO v_opening_balance, v_closing_balance
FROM financial.bank_statements
WHERE id = p_statement_id;
-- Sumar todas las lineas
SELECT COALESCE(SUM(amount), 0)
INTO v_lines_total
FROM financial.bank_statement_lines
WHERE statement_id = p_statement_id;
-- Calcular balance de cierre esperado
v_calculated_closing := v_opening_balance + v_lines_total;
-- Retornar si coincide (con tolerancia de 0.01)
RETURN ABS(v_calculated_closing - v_closing_balance) < 0.01;
END;
$$ LANGUAGE plpgsql;
-- =====================
-- FUNCION: Obtener progreso de conciliacion
-- Retorna porcentaje de lineas conciliadas
-- =====================
CREATE OR REPLACE FUNCTION financial.get_reconciliation_progress(p_statement_id UUID)
RETURNS NUMERIC AS $$
DECLARE
v_total_lines INTEGER;
v_reconciled_lines INTEGER;
BEGIN
SELECT COUNT(*), COUNT(*) FILTER (WHERE is_reconciled = true)
INTO v_total_lines, v_reconciled_lines
FROM financial.bank_statement_lines
WHERE statement_id = p_statement_id;
IF v_total_lines = 0 THEN
RETURN 100;
END IF;
RETURN ROUND((v_reconciled_lines::NUMERIC / v_total_lines::NUMERIC) * 100, 2);
END;
$$ LANGUAGE plpgsql;
-- =====================
-- COMENTARIOS
-- =====================
COMMENT ON TYPE financial.bank_statement_status_enum IS 'Estado del extracto bancario: borrador, en conciliacion, conciliado';
COMMENT ON TYPE financial.reconciliation_match_type_enum IS 'Tipo de regla de match: monto exacto, referencia contiene, nombre de partner';
COMMENT ON TABLE financial.bank_statements IS 'Extractos bancarios importados para conciliacion';
COMMENT ON COLUMN financial.bank_statements.bank_account_id IS 'Cuenta contable tipo banco asociada';
COMMENT ON COLUMN financial.bank_statements.statement_date IS 'Fecha del extracto bancario';
COMMENT ON COLUMN financial.bank_statements.opening_balance IS 'Saldo inicial del extracto';
COMMENT ON COLUMN financial.bank_statements.closing_balance IS 'Saldo final del extracto';
COMMENT ON COLUMN financial.bank_statements.status IS 'Estado: draft, reconciling, reconciled';
COMMENT ON COLUMN financial.bank_statements.imported_at IS 'Fecha y hora de importacion';
COMMENT ON COLUMN financial.bank_statements.reconciled_at IS 'Fecha y hora de cierre de conciliacion';
COMMENT ON TABLE financial.bank_statement_lines IS 'Lineas/movimientos del extracto bancario';
COMMENT ON COLUMN financial.bank_statement_lines.transaction_date IS 'Fecha de la transaccion';
COMMENT ON COLUMN financial.bank_statement_lines.value_date IS 'Fecha valor (cuando aplica el movimiento)';
COMMENT ON COLUMN financial.bank_statement_lines.amount IS 'Monto del movimiento (positivo=deposito, negativo=retiro)';
COMMENT ON COLUMN financial.bank_statement_lines.is_reconciled IS 'Indica si la linea ha sido conciliada';
COMMENT ON COLUMN financial.bank_statement_lines.reconciled_entry_id IS 'Linea de asiento contable con la que se concilio';
COMMENT ON COLUMN financial.bank_statement_lines.partner_id IS 'Partner detectado o asignado manualmente';
COMMENT ON TABLE financial.bank_reconciliation_rules IS 'Reglas para conciliacion automatica de movimientos';
COMMENT ON COLUMN financial.bank_reconciliation_rules.match_type IS 'Tipo de coincidencia: exact_amount, reference_contains, partner_name';
COMMENT ON COLUMN financial.bank_reconciliation_rules.match_value IS 'Valor a buscar segun el tipo de match';
COMMENT ON COLUMN financial.bank_reconciliation_rules.auto_account_id IS 'Cuenta contable para auto-generar asiento';
COMMENT ON COLUMN financial.bank_reconciliation_rules.priority IS 'Prioridad de evaluacion (mayor = primero)';
COMMENT ON FUNCTION financial.check_statement_balance(UUID) IS 'Verifica que el balance calculado coincida con opening + lines = closing';
COMMENT ON FUNCTION financial.get_reconciliation_progress(UUID) IS 'Retorna porcentaje de lineas conciliadas (0-100)';

View File

@ -1,381 +0,0 @@
-- =============================================================
-- ARCHIVO: 60-projects-timesheets.sql
-- DESCRIPCION: Schema de proyectos con soporte para timesheets
-- VERSION: 1.0.0
-- PROYECTO: ERP-Core V2
-- FECHA: 2026-01-20
-- DEPENDE DE: auth schema (tenants, users, companies)
-- =============================================================
-- =====================
-- SCHEMA: projects
-- Schema para modulo de gestion de proyectos
-- =====================
CREATE SCHEMA IF NOT EXISTS projects;
-- =====================
-- EXTENSIONES REQUERIDAS
-- =====================
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
-- =====================
-- TIPOS ENUMERADOS
-- =====================
-- Estado del proyecto
DO $$ BEGIN
CREATE TYPE projects.project_status_enum AS ENUM (
'draft', -- Borrador
'active', -- Activo
'completed', -- Completado
'cancelled', -- Cancelado
'on_hold' -- En espera
);
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
-- Privacidad del proyecto
DO $$ BEGIN
CREATE TYPE projects.project_privacy_enum AS ENUM (
'public', -- Publico (visible para todos)
'private', -- Privado (solo miembros)
'followers' -- Solo seguidores
);
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
-- Estado de la tarea
DO $$ BEGIN
CREATE TYPE projects.task_status_enum AS ENUM (
'todo', -- Por hacer
'in_progress', -- En progreso
'review', -- En revision
'done', -- Completada
'cancelled' -- Cancelada
);
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
-- Prioridad de la tarea
DO $$ BEGIN
CREATE TYPE projects.task_priority_enum AS ENUM (
'low', -- Baja
'normal', -- Normal
'high', -- Alta
'urgent' -- Urgente
);
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
-- Estado del timesheet
DO $$ BEGIN
CREATE TYPE projects.timesheet_status_enum AS ENUM (
'draft', -- Borrador
'submitted', -- Enviado para aprobacion
'approved', -- Aprobado
'rejected' -- Rechazado
);
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
-- Estado del milestone
DO $$ BEGIN
CREATE TYPE projects.milestone_status_enum AS ENUM (
'pending', -- Pendiente
'completed', -- Completado
'cancelled' -- Cancelado
);
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
-- =====================
-- TABLA: projects
-- Proyectos
-- =====================
CREATE TABLE IF NOT EXISTS projects.projects (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
-- Multi-tenant
tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE,
company_id UUID NOT NULL,
-- Identificacion
name VARCHAR(255) NOT NULL,
code VARCHAR(50),
description TEXT,
-- Responsables
manager_id UUID REFERENCES auth.users(id) ON DELETE SET NULL,
partner_id UUID, -- Cliente asociado al proyecto
-- Cuenta analitica (para integracion contable)
analytic_account_id UUID,
-- Fechas
date_start DATE,
date_end DATE,
-- Estado y configuracion
status projects.project_status_enum DEFAULT 'draft',
privacy projects.project_privacy_enum DEFAULT 'public',
allow_timesheets BOOLEAN DEFAULT TRUE,
color VARCHAR(20),
-- Audit columns
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
created_by UUID REFERENCES auth.users(id),
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
updated_by UUID REFERENCES auth.users(id),
deleted_at TIMESTAMPTZ,
deleted_by UUID REFERENCES auth.users(id),
-- Constraint de unicidad por codigo dentro de company
UNIQUE(company_id, code)
);
-- Indices para projects
CREATE INDEX IF NOT EXISTS idx_projects_tenant ON projects.projects(tenant_id);
CREATE INDEX IF NOT EXISTS idx_projects_company ON projects.projects(company_id);
CREATE INDEX IF NOT EXISTS idx_projects_manager ON projects.projects(manager_id);
CREATE INDEX IF NOT EXISTS idx_projects_partner ON projects.projects(partner_id);
CREATE INDEX IF NOT EXISTS idx_projects_status ON projects.projects(status);
CREATE INDEX IF NOT EXISTS idx_projects_active ON projects.projects(tenant_id) WHERE deleted_at IS NULL;
CREATE INDEX IF NOT EXISTS idx_projects_code ON projects.projects(code);
-- =====================
-- TABLA: project_stages
-- Etapas/columnas del tablero Kanban
-- =====================
CREATE TABLE IF NOT EXISTS projects.project_stages (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
-- Multi-tenant
tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE,
-- Puede ser global o por proyecto
project_id UUID REFERENCES projects.projects(id) ON DELETE CASCADE,
-- Identificacion
name VARCHAR(100) NOT NULL,
sequence INT DEFAULT 0,
-- Configuracion
fold BOOLEAN DEFAULT FALSE, -- Columna plegada por defecto
is_closed BOOLEAN DEFAULT FALSE, -- Indica etapa de cierre
-- Audit columns
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
);
-- Indices para project_stages
CREATE INDEX IF NOT EXISTS idx_project_stages_tenant ON projects.project_stages(tenant_id);
CREATE INDEX IF NOT EXISTS idx_project_stages_project ON projects.project_stages(project_id);
CREATE INDEX IF NOT EXISTS idx_project_stages_sequence ON projects.project_stages(sequence);
-- =====================
-- TABLA: tasks
-- Tareas del proyecto
-- =====================
CREATE TABLE IF NOT EXISTS projects.tasks (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
-- Multi-tenant
tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE,
-- Relaciones
project_id UUID NOT NULL REFERENCES projects.projects(id) ON DELETE CASCADE,
stage_id UUID REFERENCES projects.project_stages(id) ON DELETE SET NULL,
parent_id UUID REFERENCES projects.tasks(id) ON DELETE SET NULL,
-- Identificacion
name VARCHAR(255) NOT NULL,
description TEXT,
-- Asignacion
assigned_to UUID REFERENCES auth.users(id) ON DELETE SET NULL,
-- Fechas
date_deadline DATE,
-- Estimacion y seguimiento
estimated_hours DECIMAL(10,2) DEFAULT 0,
-- Estado y prioridad
priority projects.task_priority_enum DEFAULT 'normal',
status projects.task_status_enum DEFAULT 'todo',
-- Ordenamiento
sequence INT DEFAULT 0,
color VARCHAR(20),
-- Audit columns
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
created_by UUID REFERENCES auth.users(id),
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
updated_by UUID REFERENCES auth.users(id),
deleted_at TIMESTAMPTZ,
deleted_by UUID REFERENCES auth.users(id)
);
-- Indices para tasks
CREATE INDEX IF NOT EXISTS idx_tasks_tenant ON projects.tasks(tenant_id);
CREATE INDEX IF NOT EXISTS idx_tasks_project ON projects.tasks(project_id);
CREATE INDEX IF NOT EXISTS idx_tasks_stage ON projects.tasks(stage_id);
CREATE INDEX IF NOT EXISTS idx_tasks_parent ON projects.tasks(parent_id);
CREATE INDEX IF NOT EXISTS idx_tasks_assigned ON projects.tasks(assigned_to);
CREATE INDEX IF NOT EXISTS idx_tasks_status ON projects.tasks(status);
CREATE INDEX IF NOT EXISTS idx_tasks_priority ON projects.tasks(priority);
CREATE INDEX IF NOT EXISTS idx_tasks_deadline ON projects.tasks(date_deadline);
CREATE INDEX IF NOT EXISTS idx_tasks_active ON projects.tasks(tenant_id, project_id) WHERE deleted_at IS NULL;
CREATE INDEX IF NOT EXISTS idx_tasks_sequence ON projects.tasks(project_id, sequence);
-- =====================
-- TABLA: milestones
-- Hitos del proyecto
-- =====================
CREATE TABLE IF NOT EXISTS projects.milestones (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
-- Multi-tenant
tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE,
-- Relacion
project_id UUID NOT NULL REFERENCES projects.projects(id) ON DELETE CASCADE,
-- Identificacion
name VARCHAR(255) NOT NULL,
description TEXT,
-- Fecha objetivo
date_deadline DATE,
-- Estado
status projects.milestone_status_enum DEFAULT 'pending',
-- Audit columns
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
created_by UUID REFERENCES auth.users(id),
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
updated_by UUID REFERENCES auth.users(id)
);
-- Indices para milestones
CREATE INDEX IF NOT EXISTS idx_milestones_tenant ON projects.milestones(tenant_id);
CREATE INDEX IF NOT EXISTS idx_milestones_project ON projects.milestones(project_id);
CREATE INDEX IF NOT EXISTS idx_milestones_status ON projects.milestones(status);
CREATE INDEX IF NOT EXISTS idx_milestones_deadline ON projects.milestones(date_deadline);
-- =====================
-- TABLA: timesheets
-- Registro de horas trabajadas (estilo Odoo)
-- =====================
CREATE TABLE IF NOT EXISTS projects.timesheets (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
-- Multi-tenant
tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE,
company_id UUID NOT NULL,
-- Relaciones
project_id UUID NOT NULL REFERENCES projects.projects(id) ON DELETE CASCADE,
task_id UUID REFERENCES projects.tasks(id) ON DELETE SET NULL,
user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
-- Datos del registro
date DATE NOT NULL,
hours DECIMAL(5,2) NOT NULL CHECK (hours >= 0 AND hours <= 24),
description TEXT,
-- Facturacion
billable BOOLEAN DEFAULT TRUE,
invoiced BOOLEAN DEFAULT FALSE,
invoice_id UUID, -- FK a factura cuando se facture
-- Aprobacion
status projects.timesheet_status_enum DEFAULT 'draft',
approved_by UUID REFERENCES auth.users(id),
approved_at TIMESTAMPTZ,
-- Audit columns
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
created_by UUID REFERENCES auth.users(id),
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
updated_by UUID REFERENCES auth.users(id)
);
-- Indices para timesheets
CREATE INDEX IF NOT EXISTS idx_timesheets_tenant ON projects.timesheets(tenant_id);
CREATE INDEX IF NOT EXISTS idx_timesheets_company ON projects.timesheets(company_id);
CREATE INDEX IF NOT EXISTS idx_timesheets_project ON projects.timesheets(project_id);
CREATE INDEX IF NOT EXISTS idx_timesheets_task ON projects.timesheets(task_id);
CREATE INDEX IF NOT EXISTS idx_timesheets_user ON projects.timesheets(user_id);
CREATE INDEX IF NOT EXISTS idx_timesheets_user_date ON projects.timesheets(user_id, date);
CREATE INDEX IF NOT EXISTS idx_timesheets_date ON projects.timesheets(date);
CREATE INDEX IF NOT EXISTS idx_timesheets_status ON projects.timesheets(status);
CREATE INDEX IF NOT EXISTS idx_timesheets_billable ON projects.timesheets(billable) WHERE billable = TRUE;
CREATE INDEX IF NOT EXISTS idx_timesheets_not_invoiced ON projects.timesheets(project_id, invoiced) WHERE invoiced = FALSE;
CREATE INDEX IF NOT EXISTS idx_timesheets_invoice ON projects.timesheets(invoice_id);
-- =====================
-- TABLA: project_members
-- Miembros del equipo del proyecto
-- =====================
CREATE TABLE IF NOT EXISTS projects.project_members (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
-- Multi-tenant
tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE,
-- Relaciones
project_id UUID NOT NULL REFERENCES projects.projects(id) ON DELETE CASCADE,
user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
-- Rol en el proyecto
role VARCHAR(50) DEFAULT 'member', -- member, contributor, viewer
-- Audit columns
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
-- Un usuario solo puede ser miembro una vez por proyecto
UNIQUE(project_id, user_id)
);
-- Indices para project_members
CREATE INDEX IF NOT EXISTS idx_project_members_tenant ON projects.project_members(tenant_id);
CREATE INDEX IF NOT EXISTS idx_project_members_project ON projects.project_members(project_id);
CREATE INDEX IF NOT EXISTS idx_project_members_user ON projects.project_members(user_id);
-- =====================
-- COMENTARIOS
-- =====================
COMMENT ON SCHEMA projects IS 'Schema para gestion de proyectos: proyectos, tareas, timesheets, milestones';
COMMENT ON TABLE projects.projects IS 'Proyectos con seguimiento de tareas y timesheets';
COMMENT ON COLUMN projects.projects.allow_timesheets IS 'TRUE si el proyecto permite registro de horas';
COMMENT ON COLUMN projects.projects.analytic_account_id IS 'Cuenta analitica para integracion contable';
COMMENT ON TABLE projects.project_stages IS 'Etapas del tablero Kanban (columnas)';
COMMENT ON COLUMN projects.project_stages.fold IS 'TRUE si la columna debe mostrarse plegada';
COMMENT ON COLUMN projects.project_stages.is_closed IS 'TRUE si representa una etapa de cierre/completado';
COMMENT ON TABLE projects.tasks IS 'Tareas asignables a proyectos';
COMMENT ON COLUMN projects.tasks.estimated_hours IS 'Horas estimadas para completar la tarea';
COMMENT ON COLUMN projects.tasks.parent_id IS 'Referencia a tarea padre para subtareas';
COMMENT ON TABLE projects.milestones IS 'Hitos importantes del proyecto';
COMMENT ON TABLE projects.timesheets IS 'Registro de horas trabajadas estilo Odoo';
COMMENT ON COLUMN projects.timesheets.hours IS 'Horas trabajadas (0-24 por dia)';
COMMENT ON COLUMN projects.timesheets.billable IS 'TRUE si las horas son facturables al cliente';
COMMENT ON COLUMN projects.timesheets.invoiced IS 'TRUE si ya fue incluido en una factura';
COMMENT ON COLUMN projects.timesheets.status IS 'Flujo de aprobacion: draft -> submitted -> approved/rejected';
COMMENT ON TABLE projects.project_members IS 'Miembros del equipo del proyecto';
COMMENT ON COLUMN projects.project_members.role IS 'Rol: member (puede editar), contributor (puede agregar), viewer (solo lectura)';

View File

@ -1,259 +0,0 @@
-- =============================================================
-- 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', '', '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', '', '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
-- =====================