-- ===================================================== -- SCHEMA: financial -- PROPÓSITO: Contabilidad, facturas, pagos, finanzas -- MÓDULOS: MGN-004 (Financiero Básico) -- FECHA: 2025-11-24 -- ===================================================== -- Crear schema CREATE SCHEMA IF NOT EXISTS financial; -- ===================================================== -- TYPES (ENUMs) -- ===================================================== CREATE TYPE financial.account_type AS ENUM ( 'asset', 'liability', 'equity', 'revenue', 'expense' ); CREATE TYPE financial.journal_type AS ENUM ( 'sale', 'purchase', 'bank', 'cash', 'general' ); CREATE TYPE financial.entry_status AS ENUM ( 'draft', 'posted', 'cancelled' ); CREATE TYPE financial.invoice_type AS ENUM ( 'customer', 'supplier' ); CREATE TYPE financial.invoice_status AS ENUM ( 'draft', 'open', 'paid', 'cancelled' ); CREATE TYPE financial.payment_type AS ENUM ( 'inbound', 'outbound' ); CREATE TYPE financial.payment_method AS ENUM ( 'cash', 'bank_transfer', 'check', 'card', 'other' ); CREATE TYPE financial.payment_status AS ENUM ( 'draft', 'posted', 'reconciled', 'cancelled' ); CREATE TYPE financial.tax_type AS ENUM ( 'sales', 'purchase', 'all' ); CREATE TYPE financial.fiscal_period_status AS ENUM ( 'open', 'closed' ); -- ===================================================== -- TABLES -- ===================================================== -- Tabla: account_types (Tipos de cuenta contable) CREATE TABLE financial.account_types ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), code VARCHAR(20) NOT NULL UNIQUE, name VARCHAR(100) NOT NULL, account_type financial.account_type NOT NULL, description TEXT, -- Sin tenant_id: catálogo global created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ); -- Tabla: accounts (Plan de cuentas) CREATE TABLE financial.accounts ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, company_id UUID NOT NULL REFERENCES auth.companies(id) ON DELETE CASCADE, code VARCHAR(50) NOT NULL, name VARCHAR(255) NOT NULL, account_type_id UUID NOT NULL REFERENCES financial.account_types(id), parent_id UUID REFERENCES financial.accounts(id), -- Configuración currency_id UUID REFERENCES core.currencies(id), is_reconcilable BOOLEAN DEFAULT FALSE, -- ¿Permite conciliación? is_deprecated BOOLEAN DEFAULT FALSE, -- Notas notes TEXT, -- Auditoría created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, created_by UUID REFERENCES auth.users(id), updated_at TIMESTAMP, updated_by UUID REFERENCES auth.users(id), deleted_at TIMESTAMP, deleted_by UUID REFERENCES auth.users(id), CONSTRAINT uq_accounts_code_company UNIQUE (company_id, code), CONSTRAINT chk_accounts_no_self_parent CHECK (id != parent_id) ); -- Tabla: journals (Diarios contables) CREATE TABLE financial.journals ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, company_id UUID NOT NULL REFERENCES auth.companies(id) ON DELETE CASCADE, name VARCHAR(255) NOT NULL, code VARCHAR(20) NOT NULL, journal_type financial.journal_type NOT NULL, -- Configuración default_account_id UUID REFERENCES financial.accounts(id), sequence_id UUID REFERENCES core.sequences(id), currency_id UUID REFERENCES core.currencies(id), -- Control active BOOLEAN NOT NULL DEFAULT TRUE, -- Auditoría created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, created_by UUID REFERENCES auth.users(id), updated_at TIMESTAMP, updated_by UUID REFERENCES auth.users(id), deleted_at TIMESTAMP, deleted_by UUID REFERENCES auth.users(id), CONSTRAINT uq_journals_code_company UNIQUE (company_id, code) ); -- Tabla: fiscal_years (Años fiscales) CREATE TABLE financial.fiscal_years ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, company_id UUID NOT NULL REFERENCES auth.companies(id) ON DELETE CASCADE, name VARCHAR(100) NOT NULL, code VARCHAR(20) NOT NULL, start_date DATE NOT NULL, end_date DATE NOT NULL, status financial.fiscal_period_status NOT NULL DEFAULT 'open', -- Auditoría created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, created_by UUID REFERENCES auth.users(id), updated_at TIMESTAMP, updated_by UUID REFERENCES auth.users(id), CONSTRAINT uq_fiscal_years_code_company UNIQUE (company_id, code), CONSTRAINT chk_fiscal_years_dates CHECK (end_date > start_date) ); -- Tabla: fiscal_periods (Períodos fiscales - meses) CREATE TABLE financial.fiscal_periods ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, fiscal_year_id UUID NOT NULL REFERENCES financial.fiscal_years(id) ON DELETE CASCADE, name VARCHAR(100) NOT NULL, code VARCHAR(20) NOT NULL, start_date DATE NOT NULL, end_date DATE NOT NULL, status financial.fiscal_period_status NOT NULL DEFAULT 'open', -- Auditoría created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, created_by UUID REFERENCES auth.users(id), updated_at TIMESTAMP, updated_by UUID REFERENCES auth.users(id), CONSTRAINT uq_fiscal_periods_code_year UNIQUE (fiscal_year_id, code), CONSTRAINT chk_fiscal_periods_dates CHECK (end_date > start_date) ); -- Tabla: journal_entries (Asientos contables) CREATE TABLE financial.journal_entries ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, company_id UUID NOT NULL REFERENCES auth.companies(id) ON DELETE CASCADE, journal_id UUID NOT NULL REFERENCES financial.journals(id), name VARCHAR(100) NOT NULL, -- Número de asiento ref VARCHAR(255), -- Referencia externa date DATE NOT NULL, status financial.entry_status NOT NULL DEFAULT 'draft', -- Metadatos notes TEXT, -- Auditoría created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, created_by UUID REFERENCES auth.users(id), updated_at TIMESTAMP, updated_by UUID REFERENCES auth.users(id), posted_at TIMESTAMP, posted_by UUID REFERENCES auth.users(id), cancelled_at TIMESTAMP, cancelled_by UUID REFERENCES auth.users(id), CONSTRAINT uq_journal_entries_name_journal UNIQUE (journal_id, name) ); -- Tabla: journal_entry_lines (Líneas de asiento contable) CREATE TABLE financial.journal_entry_lines ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, entry_id UUID NOT NULL REFERENCES financial.journal_entries(id) ON DELETE CASCADE, account_id UUID NOT NULL REFERENCES financial.accounts(id), partner_id UUID REFERENCES core.partners(id), -- Montos debit DECIMAL(15, 2) NOT NULL DEFAULT 0, credit DECIMAL(15, 2) NOT NULL DEFAULT 0, -- Analítica analytic_account_id UUID, -- FK a analytics.analytic_accounts (se crea después) -- Descripción description TEXT, ref VARCHAR(255), -- Multi-moneda currency_id UUID REFERENCES core.currencies(id), amount_currency DECIMAL(15, 2), -- Auditoría created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, CONSTRAINT chk_journal_lines_debit_positive CHECK (debit >= 0), CONSTRAINT chk_journal_lines_credit_positive CHECK (credit >= 0), CONSTRAINT chk_journal_lines_not_both CHECK ( (debit > 0 AND credit = 0) OR (credit > 0 AND debit = 0) ) ); -- Índices para journal_entry_lines CREATE INDEX idx_journal_entry_lines_tenant_id ON financial.journal_entry_lines(tenant_id); CREATE INDEX idx_journal_entry_lines_entry_id ON financial.journal_entry_lines(entry_id); CREATE INDEX idx_journal_entry_lines_account_id ON financial.journal_entry_lines(account_id); -- RLS para journal_entry_lines ALTER TABLE financial.journal_entry_lines ENABLE ROW LEVEL SECURITY; CREATE POLICY tenant_isolation_journal_entry_lines ON financial.journal_entry_lines USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid); -- Tabla: taxes (Impuestos) CREATE TABLE financial.taxes ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, company_id UUID NOT NULL REFERENCES auth.companies(id) ON DELETE CASCADE, name VARCHAR(100) NOT NULL, code VARCHAR(20) NOT NULL, rate DECIMAL(5, 4) NOT NULL, -- 0.1600 para 16% tax_type financial.tax_type NOT NULL, -- Configuración contable account_id UUID REFERENCES financial.accounts(id), -- Control active BOOLEAN NOT NULL DEFAULT TRUE, -- Auditoría created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, created_by UUID REFERENCES auth.users(id), updated_at TIMESTAMP, updated_by UUID REFERENCES auth.users(id), CONSTRAINT uq_taxes_code_company UNIQUE (company_id, code), CONSTRAINT chk_taxes_rate CHECK (rate >= 0 AND rate <= 1) ); -- Tabla: payment_terms (Términos de pago) CREATE TABLE financial.payment_terms ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, company_id UUID NOT NULL REFERENCES auth.companies(id) ON DELETE CASCADE, name VARCHAR(100) NOT NULL, code VARCHAR(20) NOT NULL, -- Configuración de términos (JSON) -- Ejemplo: [{"days": 0, "percent": 100}] = Pago inmediato -- Ejemplo: [{"days": 30, "percent": 100}] = 30 días -- Ejemplo: [{"days": 15, "percent": 50}, {"days": 30, "percent": 50}] = 50% a 15 días, 50% a 30 días terms JSONB NOT NULL DEFAULT '[]', -- Control active BOOLEAN NOT NULL DEFAULT TRUE, -- Auditoría created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, created_by UUID REFERENCES auth.users(id), updated_at TIMESTAMP, updated_by UUID REFERENCES auth.users(id), CONSTRAINT uq_payment_terms_code_company UNIQUE (company_id, code) ); -- Tabla: invoices (Facturas) CREATE TABLE financial.invoices ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, company_id UUID NOT NULL REFERENCES auth.companies(id) ON DELETE CASCADE, partner_id UUID NOT NULL REFERENCES core.partners(id), invoice_type financial.invoice_type NOT NULL, -- Numeración number VARCHAR(100), -- Número de factura (generado al validar) ref VARCHAR(100), -- Referencia del partner -- Fechas invoice_date DATE NOT NULL, due_date DATE, -- Montos currency_id UUID NOT NULL REFERENCES core.currencies(id), amount_untaxed DECIMAL(15, 2) NOT NULL DEFAULT 0, amount_tax DECIMAL(15, 2) NOT NULL DEFAULT 0, amount_total DECIMAL(15, 2) NOT NULL DEFAULT 0, amount_paid DECIMAL(15, 2) NOT NULL DEFAULT 0, amount_residual DECIMAL(15, 2) NOT NULL DEFAULT 0, -- Estado status financial.invoice_status NOT NULL DEFAULT 'draft', -- Configuración payment_term_id UUID REFERENCES financial.payment_terms(id), journal_id UUID REFERENCES financial.journals(id), -- Asiento contable (generado al validar) journal_entry_id UUID REFERENCES financial.journal_entries(id), -- Notas notes TEXT, -- Auditoría created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, created_by UUID REFERENCES auth.users(id), updated_at TIMESTAMP, updated_by UUID REFERENCES auth.users(id), validated_at TIMESTAMP, validated_by UUID REFERENCES auth.users(id), cancelled_at TIMESTAMP, cancelled_by UUID REFERENCES auth.users(id), CONSTRAINT uq_invoices_number_company UNIQUE (company_id, number), CONSTRAINT chk_invoices_amounts CHECK ( amount_total = amount_untaxed + amount_tax ), CONSTRAINT chk_invoices_residual CHECK ( amount_residual = amount_total - amount_paid ) ); -- Tabla: invoice_lines (Líneas de factura) CREATE TABLE financial.invoice_lines ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, invoice_id UUID NOT NULL REFERENCES financial.invoices(id) ON DELETE CASCADE, product_id UUID, -- FK a inventory.products (se crea después) description TEXT NOT NULL, -- Cantidades y precios quantity DECIMAL(12, 4) NOT NULL DEFAULT 1, uom_id UUID REFERENCES core.uom(id), price_unit DECIMAL(15, 4) NOT NULL, -- Impuestos (array de tax_ids) tax_ids UUID[] DEFAULT '{}', -- Montos calculados amount_untaxed DECIMAL(15, 2) NOT NULL, amount_tax DECIMAL(15, 2) NOT NULL, amount_total DECIMAL(15, 2) NOT NULL, -- Contabilidad account_id UUID REFERENCES financial.accounts(id), analytic_account_id UUID, -- FK a analytics.analytic_accounts -- Auditoría created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP, CONSTRAINT chk_invoice_lines_quantity CHECK (quantity > 0), CONSTRAINT chk_invoice_lines_amounts CHECK ( amount_total = amount_untaxed + amount_tax ) ); -- Índices para invoice_lines CREATE INDEX idx_invoice_lines_tenant_id ON financial.invoice_lines(tenant_id); CREATE INDEX idx_invoice_lines_invoice_id ON financial.invoice_lines(invoice_id); CREATE INDEX idx_invoice_lines_product_id ON financial.invoice_lines(product_id); -- RLS para invoice_lines ALTER TABLE financial.invoice_lines ENABLE ROW LEVEL SECURITY; CREATE POLICY tenant_isolation_invoice_lines ON financial.invoice_lines USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid); -- Tabla: payments (Pagos) CREATE TABLE financial.payments ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, company_id UUID NOT NULL REFERENCES auth.companies(id) ON DELETE CASCADE, partner_id UUID NOT NULL REFERENCES core.partners(id), payment_type financial.payment_type NOT NULL, payment_method financial.payment_method NOT NULL, -- Monto amount DECIMAL(15, 2) NOT NULL, currency_id UUID NOT NULL REFERENCES core.currencies(id), -- Fecha y referencia payment_date DATE NOT NULL, ref VARCHAR(255), -- Estado status financial.payment_status NOT NULL DEFAULT 'draft', -- Configuración journal_id UUID NOT NULL REFERENCES financial.journals(id), -- Asiento contable (generado al validar) journal_entry_id UUID REFERENCES financial.journal_entries(id), -- Notas notes TEXT, -- Auditoría created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, created_by UUID REFERENCES auth.users(id), updated_at TIMESTAMP, updated_by UUID REFERENCES auth.users(id), posted_at TIMESTAMP, posted_by UUID REFERENCES auth.users(id), CONSTRAINT chk_payments_amount CHECK (amount > 0) ); -- Tabla: payment_invoice (Conciliación pagos-facturas) CREATE TABLE financial.payment_invoice ( payment_id UUID NOT NULL REFERENCES financial.payments(id) ON DELETE CASCADE, invoice_id UUID NOT NULL REFERENCES financial.invoices(id) ON DELETE CASCADE, amount DECIMAL(15, 2) NOT NULL, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (payment_id, invoice_id), CONSTRAINT chk_payment_invoice_amount CHECK (amount > 0) ); -- Tabla: bank_accounts (Cuentas bancarias) CREATE TABLE financial.bank_accounts ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, company_id UUID REFERENCES auth.companies(id), partner_id UUID REFERENCES core.partners(id), -- Puede ser de la empresa o de un partner bank_name VARCHAR(255) NOT NULL, account_number VARCHAR(50) NOT NULL, account_holder VARCHAR(255), -- Configuración currency_id UUID REFERENCES core.currencies(id), journal_id UUID REFERENCES financial.journals(id), -- Diario asociado (si es cuenta de la empresa) -- Control active BOOLEAN NOT NULL DEFAULT TRUE, -- Auditoría created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, created_by UUID REFERENCES auth.users(id), updated_at TIMESTAMP, updated_by UUID REFERENCES auth.users(id) ); -- Tabla: reconciliations (Conciliaciones bancarias) CREATE TABLE financial.reconciliations ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, company_id UUID NOT NULL REFERENCES auth.companies(id) ON DELETE CASCADE, bank_account_id UUID NOT NULL REFERENCES financial.bank_accounts(id), -- Período de conciliación start_date DATE NOT NULL, end_date DATE NOT NULL, -- Saldos balance_start DECIMAL(15, 2) NOT NULL, balance_end_real DECIMAL(15, 2) NOT NULL, -- Saldo real del banco balance_end_computed DECIMAL(15, 2) NOT NULL, -- Saldo calculado -- Líneas conciliadas (array de journal_entry_line_ids) reconciled_line_ids UUID[] DEFAULT '{}', -- Estado status financial.entry_status NOT NULL DEFAULT 'draft', -- Auditoría created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, created_by UUID REFERENCES auth.users(id), updated_at TIMESTAMP, updated_by UUID REFERENCES auth.users(id), validated_at TIMESTAMP, validated_by UUID REFERENCES auth.users(id), CONSTRAINT chk_reconciliations_dates CHECK (end_date >= start_date) ); -- ===================================================== -- INDICES -- ===================================================== -- Account Types CREATE INDEX idx_account_types_code ON financial.account_types(code); -- Accounts CREATE INDEX idx_accounts_tenant_id ON financial.accounts(tenant_id); CREATE INDEX idx_accounts_company_id ON financial.accounts(company_id); CREATE INDEX idx_accounts_code ON financial.accounts(code); CREATE INDEX idx_accounts_parent_id ON financial.accounts(parent_id); CREATE INDEX idx_accounts_type_id ON financial.accounts(account_type_id); -- Journals CREATE INDEX idx_journals_tenant_id ON financial.journals(tenant_id); CREATE INDEX idx_journals_company_id ON financial.journals(company_id); CREATE INDEX idx_journals_code ON financial.journals(code); CREATE INDEX idx_journals_type ON financial.journals(journal_type); -- Fiscal Years CREATE INDEX idx_fiscal_years_tenant_id ON financial.fiscal_years(tenant_id); CREATE INDEX idx_fiscal_years_company_id ON financial.fiscal_years(company_id); CREATE INDEX idx_fiscal_years_dates ON financial.fiscal_years(start_date, end_date); -- Fiscal Periods CREATE INDEX idx_fiscal_periods_tenant_id ON financial.fiscal_periods(tenant_id); CREATE INDEX idx_fiscal_periods_year_id ON financial.fiscal_periods(fiscal_year_id); CREATE INDEX idx_fiscal_periods_dates ON financial.fiscal_periods(start_date, end_date); -- Journal Entries CREATE INDEX idx_journal_entries_tenant_id ON financial.journal_entries(tenant_id); CREATE INDEX idx_journal_entries_company_id ON financial.journal_entries(company_id); CREATE INDEX idx_journal_entries_journal_id ON financial.journal_entries(journal_id); CREATE INDEX idx_journal_entries_date ON financial.journal_entries(date); CREATE INDEX idx_journal_entries_status ON financial.journal_entries(status); -- Journal Entry Lines CREATE INDEX idx_journal_entry_lines_entry_id ON financial.journal_entry_lines(entry_id); CREATE INDEX idx_journal_entry_lines_account_id ON financial.journal_entry_lines(account_id); CREATE INDEX idx_journal_entry_lines_partner_id ON financial.journal_entry_lines(partner_id); CREATE INDEX idx_journal_entry_lines_analytic ON financial.journal_entry_lines(analytic_account_id); -- Taxes CREATE INDEX idx_taxes_tenant_id ON financial.taxes(tenant_id); CREATE INDEX idx_taxes_company_id ON financial.taxes(company_id); CREATE INDEX idx_taxes_code ON financial.taxes(code); CREATE INDEX idx_taxes_type ON financial.taxes(tax_type); CREATE INDEX idx_taxes_active ON financial.taxes(active) WHERE active = TRUE; -- Payment Terms CREATE INDEX idx_payment_terms_tenant_id ON financial.payment_terms(tenant_id); CREATE INDEX idx_payment_terms_company_id ON financial.payment_terms(company_id); -- Invoices CREATE INDEX idx_invoices_tenant_id ON financial.invoices(tenant_id); CREATE INDEX idx_invoices_company_id ON financial.invoices(company_id); CREATE INDEX idx_invoices_partner_id ON financial.invoices(partner_id); CREATE INDEX idx_invoices_type ON financial.invoices(invoice_type); CREATE INDEX idx_invoices_status ON financial.invoices(status); CREATE INDEX idx_invoices_number ON financial.invoices(number); CREATE INDEX idx_invoices_date ON financial.invoices(invoice_date); CREATE INDEX idx_invoices_due_date ON financial.invoices(due_date); -- Invoice Lines CREATE INDEX idx_invoice_lines_invoice_id ON financial.invoice_lines(invoice_id); CREATE INDEX idx_invoice_lines_product_id ON financial.invoice_lines(product_id); CREATE INDEX idx_invoice_lines_account_id ON financial.invoice_lines(account_id); -- Payments CREATE INDEX idx_payments_tenant_id ON financial.payments(tenant_id); CREATE INDEX idx_payments_company_id ON financial.payments(company_id); CREATE INDEX idx_payments_partner_id ON financial.payments(partner_id); CREATE INDEX idx_payments_type ON financial.payments(payment_type); CREATE INDEX idx_payments_status ON financial.payments(status); CREATE INDEX idx_payments_date ON financial.payments(payment_date); -- Payment Invoice CREATE INDEX idx_payment_invoice_payment_id ON financial.payment_invoice(payment_id); CREATE INDEX idx_payment_invoice_invoice_id ON financial.payment_invoice(invoice_id); -- Bank Accounts CREATE INDEX idx_bank_accounts_tenant_id ON financial.bank_accounts(tenant_id); CREATE INDEX idx_bank_accounts_company_id ON financial.bank_accounts(company_id); CREATE INDEX idx_bank_accounts_partner_id ON financial.bank_accounts(partner_id); -- Reconciliations CREATE INDEX idx_reconciliations_tenant_id ON financial.reconciliations(tenant_id); CREATE INDEX idx_reconciliations_company_id ON financial.reconciliations(company_id); CREATE INDEX idx_reconciliations_bank_account_id ON financial.reconciliations(bank_account_id); CREATE INDEX idx_reconciliations_dates ON financial.reconciliations(start_date, end_date); -- ===================================================== -- FUNCTIONS -- ===================================================== -- Función: validate_entry_balance -- Valida que un asiento esté balanceado (debit = credit) CREATE OR REPLACE FUNCTION financial.validate_entry_balance(p_entry_id UUID) RETURNS BOOLEAN AS $$ DECLARE v_total_debit DECIMAL; v_total_credit DECIMAL; BEGIN SELECT COALESCE(SUM(debit), 0), COALESCE(SUM(credit), 0) INTO v_total_debit, v_total_credit FROM financial.journal_entry_lines WHERE entry_id = p_entry_id; IF v_total_debit != v_total_credit THEN RAISE EXCEPTION 'Journal entry % is not balanced: debit=% credit=%', p_entry_id, v_total_debit, v_total_credit; END IF; RETURN TRUE; END; $$ LANGUAGE plpgsql; COMMENT ON FUNCTION financial.validate_entry_balance IS 'Valida que un asiento contable esté balanceado (debit = credit)'; -- Función: post_journal_entry -- Contabiliza un asiento (cambiar estado a posted) CREATE OR REPLACE FUNCTION financial.post_journal_entry(p_entry_id UUID) RETURNS VOID AS $$ BEGIN -- Validar balance PERFORM financial.validate_entry_balance(p_entry_id); -- Actualizar estado UPDATE financial.journal_entries SET status = 'posted', posted_at = CURRENT_TIMESTAMP, posted_by = get_current_user_id() WHERE id = p_entry_id AND status = 'draft'; IF NOT FOUND THEN RAISE EXCEPTION 'Journal entry % not found or already posted', p_entry_id; END IF; END; $$ LANGUAGE plpgsql; COMMENT ON FUNCTION financial.post_journal_entry IS 'Contabiliza un asiento contable después de validar su balance'; -- Función: calculate_invoice_totals -- Calcula los totales de una factura a partir de sus líneas CREATE OR REPLACE FUNCTION financial.calculate_invoice_totals(p_invoice_id UUID) RETURNS VOID AS $$ DECLARE v_amount_untaxed DECIMAL; v_amount_tax DECIMAL; v_amount_total DECIMAL; BEGIN SELECT COALESCE(SUM(amount_untaxed), 0), COALESCE(SUM(amount_tax), 0), COALESCE(SUM(amount_total), 0) INTO v_amount_untaxed, v_amount_tax, v_amount_total FROM financial.invoice_lines WHERE invoice_id = p_invoice_id; UPDATE financial.invoices SET amount_untaxed = v_amount_untaxed, amount_tax = v_amount_tax, amount_total = v_amount_total, amount_residual = v_amount_total - amount_paid, updated_at = CURRENT_TIMESTAMP, updated_by = get_current_user_id() WHERE id = p_invoice_id; END; $$ LANGUAGE plpgsql; COMMENT ON FUNCTION financial.calculate_invoice_totals IS 'Calcula los totales de una factura a partir de sus líneas'; -- Función: update_invoice_paid_amount -- Actualiza el monto pagado de una factura CREATE OR REPLACE FUNCTION financial.update_invoice_paid_amount(p_invoice_id UUID) RETURNS VOID AS $$ DECLARE v_amount_paid DECIMAL; BEGIN SELECT COALESCE(SUM(amount), 0) INTO v_amount_paid FROM financial.payment_invoice WHERE invoice_id = p_invoice_id; UPDATE financial.invoices SET amount_paid = v_amount_paid, amount_residual = amount_total - v_amount_paid, status = CASE WHEN v_amount_paid >= amount_total THEN 'paid'::financial.invoice_status WHEN v_amount_paid > 0 THEN 'open'::financial.invoice_status ELSE status END WHERE id = p_invoice_id; END; $$ LANGUAGE plpgsql; COMMENT ON FUNCTION financial.update_invoice_paid_amount IS 'Actualiza el monto pagado y estado de una factura'; -- ===================================================== -- TRIGGERS -- ===================================================== -- Trigger: Actualizar updated_at CREATE TRIGGER trg_accounts_updated_at BEFORE UPDATE ON financial.accounts FOR EACH ROW EXECUTE FUNCTION auth.update_updated_at_column(); CREATE TRIGGER trg_journals_updated_at BEFORE UPDATE ON financial.journals FOR EACH ROW EXECUTE FUNCTION auth.update_updated_at_column(); CREATE TRIGGER trg_fiscal_years_updated_at BEFORE UPDATE ON financial.fiscal_years FOR EACH ROW EXECUTE FUNCTION auth.update_updated_at_column(); CREATE TRIGGER trg_fiscal_periods_updated_at BEFORE UPDATE ON financial.fiscal_periods FOR EACH ROW EXECUTE FUNCTION auth.update_updated_at_column(); CREATE TRIGGER trg_journal_entries_updated_at BEFORE UPDATE ON financial.journal_entries FOR EACH ROW EXECUTE FUNCTION auth.update_updated_at_column(); CREATE TRIGGER trg_taxes_updated_at BEFORE UPDATE ON financial.taxes FOR EACH ROW EXECUTE FUNCTION auth.update_updated_at_column(); CREATE TRIGGER trg_payment_terms_updated_at BEFORE UPDATE ON financial.payment_terms FOR EACH ROW EXECUTE FUNCTION auth.update_updated_at_column(); CREATE TRIGGER trg_invoices_updated_at BEFORE UPDATE ON financial.invoices FOR EACH ROW EXECUTE FUNCTION auth.update_updated_at_column(); CREATE TRIGGER trg_payments_updated_at BEFORE UPDATE ON financial.payments FOR EACH ROW EXECUTE FUNCTION auth.update_updated_at_column(); CREATE TRIGGER trg_bank_accounts_updated_at BEFORE UPDATE ON financial.bank_accounts FOR EACH ROW EXECUTE FUNCTION auth.update_updated_at_column(); CREATE TRIGGER trg_reconciliations_updated_at BEFORE UPDATE ON financial.reconciliations FOR EACH ROW EXECUTE FUNCTION auth.update_updated_at_column(); -- Trigger: Validar balance antes de contabilizar CREATE OR REPLACE FUNCTION financial.trg_validate_entry_before_post() RETURNS TRIGGER AS $$ BEGIN IF NEW.status = 'posted' AND OLD.status = 'draft' THEN PERFORM financial.validate_entry_balance(NEW.id); END IF; RETURN NEW; END; $$ LANGUAGE plpgsql; CREATE TRIGGER trg_journal_entries_validate_balance BEFORE UPDATE OF status ON financial.journal_entries FOR EACH ROW EXECUTE FUNCTION financial.trg_validate_entry_before_post(); -- Trigger: Actualizar totales de factura al cambiar líneas CREATE OR REPLACE FUNCTION financial.trg_update_invoice_totals() RETURNS TRIGGER AS $$ BEGIN IF TG_OP = 'DELETE' THEN PERFORM financial.calculate_invoice_totals(OLD.invoice_id); ELSE PERFORM financial.calculate_invoice_totals(NEW.invoice_id); END IF; RETURN NULL; END; $$ LANGUAGE plpgsql; CREATE TRIGGER trg_invoice_lines_update_totals AFTER INSERT OR UPDATE OR DELETE ON financial.invoice_lines FOR EACH ROW EXECUTE FUNCTION financial.trg_update_invoice_totals(); -- Trigger: Actualizar monto pagado al conciliar CREATE OR REPLACE FUNCTION financial.trg_update_invoice_paid() RETURNS TRIGGER AS $$ BEGIN IF TG_OP = 'DELETE' THEN PERFORM financial.update_invoice_paid_amount(OLD.invoice_id); ELSE PERFORM financial.update_invoice_paid_amount(NEW.invoice_id); END IF; RETURN NULL; END; $$ LANGUAGE plpgsql; CREATE TRIGGER trg_payment_invoice_update_paid AFTER INSERT OR UPDATE OR DELETE ON financial.payment_invoice FOR EACH ROW EXECUTE FUNCTION financial.trg_update_invoice_paid(); -- ===================================================== -- TRACKING AUTOMÁTICO (mail.thread pattern) -- ===================================================== -- Trigger: Tracking automático para facturas CREATE TRIGGER track_invoice_changes AFTER INSERT OR UPDATE OR DELETE ON financial.invoices FOR EACH ROW EXECUTE FUNCTION system.track_field_changes(); COMMENT ON TRIGGER track_invoice_changes ON financial.invoices IS 'Registra automáticamente cambios en facturas (estado, monto, cliente, fechas)'; -- Trigger: Tracking automático para asientos contables CREATE TRIGGER track_journal_entry_changes AFTER INSERT OR UPDATE OR DELETE ON financial.journal_entries FOR EACH ROW EXECUTE FUNCTION system.track_field_changes(); COMMENT ON TRIGGER track_journal_entry_changes ON financial.journal_entries IS 'Registra automáticamente cambios en asientos contables (estado, fecha, diario)'; -- ===================================================== -- ROW LEVEL SECURITY (RLS) -- ===================================================== ALTER TABLE financial.accounts ENABLE ROW LEVEL SECURITY; ALTER TABLE financial.journals ENABLE ROW LEVEL SECURITY; ALTER TABLE financial.fiscal_years ENABLE ROW LEVEL SECURITY; ALTER TABLE financial.fiscal_periods ENABLE ROW LEVEL SECURITY; ALTER TABLE financial.journal_entries ENABLE ROW LEVEL SECURITY; ALTER TABLE financial.taxes ENABLE ROW LEVEL SECURITY; ALTER TABLE financial.payment_terms ENABLE ROW LEVEL SECURITY; ALTER TABLE financial.invoices ENABLE ROW LEVEL SECURITY; ALTER TABLE financial.payments ENABLE ROW LEVEL SECURITY; ALTER TABLE financial.bank_accounts ENABLE ROW LEVEL SECURITY; ALTER TABLE financial.reconciliations ENABLE ROW LEVEL SECURITY; CREATE POLICY tenant_isolation_accounts ON financial.accounts USING (tenant_id = get_current_tenant_id()); CREATE POLICY tenant_isolation_journals ON financial.journals USING (tenant_id = get_current_tenant_id()); CREATE POLICY tenant_isolation_fiscal_years ON financial.fiscal_years USING (tenant_id = get_current_tenant_id()); CREATE POLICY tenant_isolation_fiscal_periods ON financial.fiscal_periods USING (tenant_id = get_current_tenant_id()); CREATE POLICY tenant_isolation_journal_entries ON financial.journal_entries USING (tenant_id = get_current_tenant_id()); CREATE POLICY tenant_isolation_taxes ON financial.taxes USING (tenant_id = get_current_tenant_id()); CREATE POLICY tenant_isolation_payment_terms ON financial.payment_terms USING (tenant_id = get_current_tenant_id()); CREATE POLICY tenant_isolation_invoices ON financial.invoices USING (tenant_id = get_current_tenant_id()); CREATE POLICY tenant_isolation_payments ON financial.payments USING (tenant_id = get_current_tenant_id()); CREATE POLICY tenant_isolation_bank_accounts ON financial.bank_accounts USING (tenant_id = get_current_tenant_id()); CREATE POLICY tenant_isolation_reconciliations ON financial.reconciliations USING (tenant_id = get_current_tenant_id()); -- ===================================================== -- SEED DATA -- ===================================================== -- Tipos de cuenta estándar INSERT INTO financial.account_types (code, name, account_type, description) VALUES ('ASSET_CASH', 'Cash and Cash Equivalents', 'asset', 'Efectivo y equivalentes'), ('ASSET_RECEIVABLE', 'Accounts Receivable', 'asset', 'Cuentas por cobrar'), ('ASSET_CURRENT', 'Current Assets', 'asset', 'Activos circulantes'), ('ASSET_FIXED', 'Fixed Assets', 'asset', 'Activos fijos'), ('LIABILITY_PAYABLE', 'Accounts Payable', 'liability', 'Cuentas por pagar'), ('LIABILITY_CURRENT', 'Current Liabilities', 'liability', 'Pasivos circulantes'), ('LIABILITY_LONG', 'Long-term Liabilities', 'liability', 'Pasivos a largo plazo'), ('EQUITY_CAPITAL', 'Capital', 'equity', 'Capital social'), ('EQUITY_RETAINED', 'Retained Earnings', 'equity', 'Utilidades retenidas'), ('REVENUE_SALES', 'Sales Revenue', 'revenue', 'Ingresos por ventas'), ('REVENUE_OTHER', 'Other Revenue', 'revenue', 'Otros ingresos'), ('EXPENSE_COGS', 'Cost of Goods Sold', 'expense', 'Costo de ventas'), ('EXPENSE_OPERATING', 'Operating Expenses', 'expense', 'Gastos operativos'), ('EXPENSE_ADMIN', 'Administrative Expenses', 'expense', 'Gastos administrativos') ON CONFLICT (code) DO NOTHING; -- ===================================================== -- COMENTARIOS -- ===================================================== COMMENT ON SCHEMA financial IS 'Schema de contabilidad, facturas, pagos y finanzas'; COMMENT ON TABLE financial.account_types IS 'Tipos de cuentas contables (asset, liability, equity, revenue, expense)'; COMMENT ON TABLE financial.accounts IS 'Plan de cuentas contables'; COMMENT ON TABLE financial.journals IS 'Diarios contables (ventas, compras, bancos, etc.)'; COMMENT ON TABLE financial.fiscal_years IS 'Años fiscales'; COMMENT ON TABLE financial.fiscal_periods IS 'Períodos fiscales (meses)'; COMMENT ON TABLE financial.journal_entries IS 'Asientos contables'; COMMENT ON TABLE financial.journal_entry_lines IS 'Líneas de asientos contables (partida doble)'; COMMENT ON TABLE financial.taxes IS 'Impuestos (IVA, retenciones, etc.)'; COMMENT ON TABLE financial.payment_terms IS 'Términos de pago (inmediato, 30 días, etc.)'; COMMENT ON TABLE financial.invoices IS 'Facturas de cliente y proveedor'; COMMENT ON TABLE financial.invoice_lines IS 'Líneas de factura'; COMMENT ON TABLE financial.payments IS 'Pagos y cobros'; COMMENT ON TABLE financial.payment_invoice IS 'Conciliación de pagos con facturas'; COMMENT ON TABLE financial.bank_accounts IS 'Cuentas bancarias de la empresa y partners'; COMMENT ON TABLE financial.reconciliations IS 'Conciliaciones bancarias'; -- ===================================================== -- FIN DEL SCHEMA FINANCIAL -- =====================================================