erp-core/docs/02-fase-core-business/MGN-010-financial/especificaciones/ET-FIN-database.md

29 KiB

DDL-SPEC: Schema core_financial

Identificacion

Campo Valor
Schema core_financial
Modulo MGN-010
Version 1.0
Estado En Diseno
Autor Requirements-Analyst
Fecha 2025-12-05

Descripcion General

El schema core_financial implementa las bases del sistema contable: plan de cuentas jerarquico, manejo multi-moneda con conversion, periodos fiscales, y asientos contables con partida doble. Sirve como fundamento para modulos financieros de verticales.

RF Cubiertos

RF Titulo Tablas
RF-FIN-001 Plan de Cuentas charts_of_accounts, accounts, account_types
RF-FIN-002 Monedas y Cambio tenant_currencies, exchange_rates
RF-FIN-003 Periodos fiscal_years, fiscal_periods
RF-FIN-004 Asientos journal_entries, journal_lines, cost_centers

Diagrama ER

erDiagram
    charts_of_accounts {
        uuid id PK
        uuid tenant_id FK
        varchar code
        varchar name
        boolean is_active
    }

    account_types {
        uuid id PK
        varchar code UK
        varchar name
        varchar classification
        varchar normal_balance
        int display_order
    }

    accounts {
        uuid id PK
        uuid chart_id FK
        uuid parent_id FK
        uuid account_type_id FK
        varchar code
        varchar name
        int level
        boolean is_detail
        decimal opening_balance
    }

    tenant_currencies {
        uuid id PK
        uuid tenant_id FK
        varchar currency_code FK
        boolean is_base
        boolean is_active
    }

    exchange_rates {
        uuid id PK
        uuid tenant_id FK
        varchar from_currency
        varchar to_currency
        decimal rate
        date effective_date
    }

    fiscal_years {
        uuid id PK
        uuid tenant_id FK
        varchar name
        date start_date
        date end_date
        varchar status
    }

    fiscal_periods {
        uuid id PK
        uuid fiscal_year_id FK
        varchar name
        int period_number
        date start_date
        date end_date
        varchar status
    }

    journal_entries {
        uuid id PK
        uuid tenant_id FK
        uuid fiscal_period_id FK
        varchar entry_number
        date entry_date
        varchar currency_code
        decimal exchange_rate
        varchar status
        text description
    }

    journal_lines {
        uuid id PK
        uuid journal_entry_id FK
        uuid account_id FK
        uuid cost_center_id FK
        decimal debit
        decimal credit
        decimal debit_base
        decimal credit_base
        text description
    }

    cost_centers {
        uuid id PK
        uuid tenant_id FK
        uuid parent_id FK
        varchar code
        varchar name
        boolean is_active
    }

    charts_of_accounts ||--o{ accounts : "contiene"
    accounts ||--o{ accounts : "padre"
    accounts }o--|| account_types : "tipo"
    fiscal_years ||--o{ fiscal_periods : "contiene"
    fiscal_periods ||--o{ journal_entries : "periodo"
    journal_entries ||--o{ journal_lines : "lineas"
    accounts ||--o{ journal_lines : "cuenta"
    cost_centers ||--o{ journal_lines : "centro_costo"

Tablas

1. account_types

Tipos de cuenta predefinidos (global).

Columna Tipo Nullable Default Descripcion
id UUID NOT NULL gen_random_uuid() PK
code VARCHAR(20) NOT NULL - Codigo unico
name VARCHAR(100) NOT NULL - Nombre
classification VARCHAR(20) NOT NULL - Clasificacion contable
normal_balance VARCHAR(10) NOT NULL - Saldo normal
affects_cash_flow BOOLEAN NOT NULL false Afecta flujo de caja
display_order INTEGER NOT NULL 0 Orden en reportes
CREATE TABLE core_financial.account_types (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    code VARCHAR(20) NOT NULL UNIQUE,
    name VARCHAR(100) NOT NULL,
    classification VARCHAR(20) NOT NULL,
    normal_balance VARCHAR(10) NOT NULL,
    affects_cash_flow BOOLEAN NOT NULL DEFAULT false,
    display_order INTEGER NOT NULL DEFAULT 0,

    CONSTRAINT chk_classification CHECK (classification IN (
        'asset', 'liability', 'equity', 'revenue', 'expense'
    )),
    CONSTRAINT chk_normal_balance CHECK (normal_balance IN ('debit', 'credit'))
);

-- Seed data
INSERT INTO core_financial.account_types (code, name, classification, normal_balance, display_order) VALUES
('ASSET_CURRENT', 'Activo Circulante', 'asset', 'debit', 1),
('ASSET_FIXED', 'Activo Fijo', 'asset', 'debit', 2),
('ASSET_OTHER', 'Otros Activos', 'asset', 'debit', 3),
('LIABILITY_CURRENT', 'Pasivo Circulante', 'liability', 'credit', 4),
('LIABILITY_LONG', 'Pasivo a Largo Plazo', 'liability', 'credit', 5),
('EQUITY', 'Capital', 'equity', 'credit', 6),
('REVENUE', 'Ingresos', 'revenue', 'credit', 7),
('EXPENSE', 'Gastos', 'expense', 'debit', 8),
('COGS', 'Costo de Ventas', 'expense', 'debit', 9),
('OTHER_INCOME', 'Otros Ingresos', 'revenue', 'credit', 10),
('OTHER_EXPENSE', 'Otros Gastos', 'expense', 'debit', 11);

2. charts_of_accounts

Planes de cuentas por tenant.

Columna Tipo Nullable Default Descripcion
id UUID NOT NULL gen_random_uuid() PK
tenant_id UUID NOT NULL - FK a tenants
code VARCHAR(20) NOT NULL - Codigo
name VARCHAR(255) NOT NULL - Nombre del plan
description TEXT NULL - Descripcion
is_active BOOLEAN NOT NULL true Activo
created_at TIMESTAMPTZ NOT NULL NOW() Fecha creacion
updated_at TIMESTAMPTZ NOT NULL NOW() Fecha actualizacion
CREATE TABLE core_financial.charts_of_accounts (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    tenant_id UUID NOT NULL,
    code VARCHAR(20) NOT NULL,
    name VARCHAR(255) NOT NULL,
    description TEXT,
    is_active BOOLEAN NOT NULL DEFAULT true,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),

    CONSTRAINT uk_charts_tenant_code UNIQUE (tenant_id, code)
);

-- RLS
ALTER TABLE core_financial.charts_of_accounts ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON core_financial.charts_of_accounts
    FOR ALL USING (tenant_id = current_setting('app.current_tenant_id')::uuid);

3. accounts

Cuentas contables jerarquicas.

Columna Tipo Nullable Default Descripcion
id UUID NOT NULL gen_random_uuid() PK
chart_id UUID NOT NULL - FK a charts_of_accounts
tenant_id UUID NOT NULL - FK a tenants (redundante para RLS)
parent_id UUID NULL - Cuenta padre
account_type_id UUID NOT NULL - FK a account_types
code VARCHAR(20) NOT NULL - Codigo cuenta
name VARCHAR(255) NOT NULL - Nombre
description TEXT NULL - Descripcion
level INTEGER NOT NULL 1 Nivel jerarquico
path LTREE NULL - Path materializado
is_detail BOOLEAN NOT NULL true Cuenta de detalle
is_bank BOOLEAN NOT NULL false Es cuenta bancaria
is_cash BOOLEAN NOT NULL false Es cuenta de caja
currency_code VARCHAR(3) NULL - Moneda (si especifica)
opening_balance DECIMAL(18,4) NOT NULL 0 Saldo inicial
current_balance DECIMAL(18,4) NOT NULL 0 Saldo actual
is_active BOOLEAN NOT NULL true Activa
created_at TIMESTAMPTZ NOT NULL NOW() Fecha creacion
updated_at TIMESTAMPTZ NOT NULL NOW() Fecha actualizacion
CREATE EXTENSION IF NOT EXISTS ltree;

CREATE TABLE core_financial.accounts (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    chart_id UUID NOT NULL REFERENCES core_financial.charts_of_accounts(id),
    tenant_id UUID NOT NULL,
    parent_id UUID REFERENCES core_financial.accounts(id),
    account_type_id UUID NOT NULL REFERENCES core_financial.account_types(id),
    code VARCHAR(20) NOT NULL,
    name VARCHAR(255) NOT NULL,
    description TEXT,
    level INTEGER NOT NULL DEFAULT 1,
    path LTREE,
    is_detail BOOLEAN NOT NULL DEFAULT true,
    is_bank BOOLEAN NOT NULL DEFAULT false,
    is_cash BOOLEAN NOT NULL DEFAULT false,
    currency_code VARCHAR(3),
    opening_balance DECIMAL(18,4) NOT NULL DEFAULT 0,
    current_balance DECIMAL(18,4) NOT NULL DEFAULT 0,
    is_active BOOLEAN NOT NULL DEFAULT true,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),

    CONSTRAINT uk_accounts_chart_code UNIQUE (chart_id, code),
    CONSTRAINT chk_accounts_level CHECK (level >= 1 AND level <= 10)
);

CREATE INDEX idx_accounts_chart ON core_financial.accounts(chart_id);
CREATE INDEX idx_accounts_parent ON core_financial.accounts(parent_id);
CREATE INDEX idx_accounts_type ON core_financial.accounts(account_type_id);
CREATE INDEX idx_accounts_path ON core_financial.accounts USING GIST (path);
CREATE INDEX idx_accounts_detail ON core_financial.accounts(chart_id, is_detail)
    WHERE is_detail = true;

-- RLS
ALTER TABLE core_financial.accounts ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON core_financial.accounts
    FOR ALL USING (tenant_id = current_setting('app.current_tenant_id')::uuid);

4. tenant_currencies

Monedas habilitadas por tenant.

Columna Tipo Nullable Default Descripcion
id UUID NOT NULL gen_random_uuid() PK
tenant_id UUID NOT NULL - FK a tenants
currency_code VARCHAR(3) NOT NULL - FK a currencies
is_base BOOLEAN NOT NULL false Moneda base
is_active BOOLEAN NOT NULL true Activa
decimal_places INTEGER NOT NULL 2 Decimales
created_at TIMESTAMPTZ NOT NULL NOW() Fecha creacion
CREATE TABLE core_financial.tenant_currencies (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    tenant_id UUID NOT NULL,
    currency_code VARCHAR(3) NOT NULL,
    is_base BOOLEAN NOT NULL DEFAULT false,
    is_active BOOLEAN NOT NULL DEFAULT true,
    decimal_places INTEGER NOT NULL DEFAULT 2,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),

    CONSTRAINT uk_tenant_currencies UNIQUE (tenant_id, currency_code),
    CONSTRAINT fk_tenant_currencies_currency
        FOREIGN KEY (currency_code) REFERENCES core_catalogs.currencies(code)
);

-- Solo una moneda base por tenant
CREATE UNIQUE INDEX idx_tenant_currencies_base ON core_financial.tenant_currencies(tenant_id)
    WHERE is_base = true;

-- RLS
ALTER TABLE core_financial.tenant_currencies ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON core_financial.tenant_currencies
    FOR ALL USING (tenant_id = current_setting('app.current_tenant_id')::uuid);

5. exchange_rates

Tipos de cambio historicos.

Columna Tipo Nullable Default Descripcion
id UUID NOT NULL gen_random_uuid() PK
tenant_id UUID NOT NULL - FK a tenants
from_currency VARCHAR(3) NOT NULL - Moneda origen
to_currency VARCHAR(3) NOT NULL - Moneda destino
rate DECIMAL(18,8) NOT NULL - Tasa de cambio
effective_date DATE NOT NULL - Fecha efectiva
source VARCHAR(50) NULL - Fuente del tipo
created_at TIMESTAMPTZ NOT NULL NOW() Fecha creacion
created_by UUID NULL - Usuario creador
CREATE TABLE core_financial.exchange_rates (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    tenant_id UUID NOT NULL,
    from_currency VARCHAR(3) NOT NULL,
    to_currency VARCHAR(3) NOT NULL,
    rate DECIMAL(18,8) NOT NULL,
    effective_date DATE NOT NULL,
    source VARCHAR(50),
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    created_by UUID,

    CONSTRAINT uk_exchange_rates UNIQUE (tenant_id, from_currency, to_currency, effective_date),
    CONSTRAINT chk_rate_positive CHECK (rate > 0)
);

CREATE INDEX idx_exchange_rates_lookup ON core_financial.exchange_rates(
    tenant_id, from_currency, to_currency, effective_date DESC
);

-- RLS
ALTER TABLE core_financial.exchange_rates ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON core_financial.exchange_rates
    FOR ALL USING (tenant_id = current_setting('app.current_tenant_id')::uuid);

6. fiscal_years

Anos fiscales.

Columna Tipo Nullable Default Descripcion
id UUID NOT NULL gen_random_uuid() PK
tenant_id UUID NOT NULL - FK a tenants
name VARCHAR(50) NOT NULL - Nombre (ej: "2025")
start_date DATE NOT NULL - Fecha inicio
end_date DATE NOT NULL - Fecha fin
status VARCHAR(20) NOT NULL 'open' Estado
closed_at TIMESTAMPTZ NULL - Fecha cierre
closed_by UUID NULL - Usuario que cerro
created_at TIMESTAMPTZ NOT NULL NOW() Fecha creacion
CREATE TABLE core_financial.fiscal_years (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    tenant_id UUID NOT NULL,
    name VARCHAR(50) NOT NULL,
    start_date DATE NOT NULL,
    end_date DATE NOT NULL,
    status VARCHAR(20) NOT NULL DEFAULT 'open',
    closed_at TIMESTAMPTZ,
    closed_by UUID,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),

    CONSTRAINT uk_fiscal_years_tenant_name UNIQUE (tenant_id, name),
    CONSTRAINT chk_fiscal_years_dates CHECK (end_date > start_date),
    CONSTRAINT chk_fiscal_years_status CHECK (status IN ('open', 'closing', 'closed'))
);

-- RLS
ALTER TABLE core_financial.fiscal_years ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON core_financial.fiscal_years
    FOR ALL USING (tenant_id = current_setting('app.current_tenant_id')::uuid);

7. fiscal_periods

Periodos contables dentro de ano fiscal.

Columna Tipo Nullable Default Descripcion
id UUID NOT NULL gen_random_uuid() PK
fiscal_year_id UUID NOT NULL - FK a fiscal_years
tenant_id UUID NOT NULL - FK a tenants
name VARCHAR(50) NOT NULL - Nombre periodo
period_number INTEGER NOT NULL - Numero secuencial
start_date DATE NOT NULL - Fecha inicio
end_date DATE NOT NULL - Fecha fin
status VARCHAR(20) NOT NULL 'open' Estado
is_adjustment BOOLEAN NOT NULL false Periodo de ajuste
closed_at TIMESTAMPTZ NULL - Fecha cierre
closed_by UUID NULL - Usuario que cerro
CREATE TABLE core_financial.fiscal_periods (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    fiscal_year_id UUID NOT NULL REFERENCES core_financial.fiscal_years(id),
    tenant_id UUID NOT NULL,
    name VARCHAR(50) NOT NULL,
    period_number INTEGER NOT NULL,
    start_date DATE NOT NULL,
    end_date DATE NOT NULL,
    status VARCHAR(20) NOT NULL DEFAULT 'open',
    is_adjustment BOOLEAN NOT NULL DEFAULT false,
    closed_at TIMESTAMPTZ,
    closed_by UUID,

    CONSTRAINT uk_fiscal_periods_year_number UNIQUE (fiscal_year_id, period_number),
    CONSTRAINT chk_fiscal_periods_dates CHECK (end_date >= start_date),
    CONSTRAINT chk_fiscal_periods_status CHECK (status IN ('open', 'closing', 'closed'))
);

CREATE INDEX idx_fiscal_periods_year ON core_financial.fiscal_periods(fiscal_year_id);
CREATE INDEX idx_fiscal_periods_date ON core_financial.fiscal_periods(tenant_id, start_date, end_date);
CREATE INDEX idx_fiscal_periods_open ON core_financial.fiscal_periods(tenant_id, status)
    WHERE status = 'open';

-- RLS
ALTER TABLE core_financial.fiscal_periods ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON core_financial.fiscal_periods
    FOR ALL USING (tenant_id = current_setting('app.current_tenant_id')::uuid);

8. cost_centers

Centros de costo.

Columna Tipo Nullable Default Descripcion
id UUID NOT NULL gen_random_uuid() PK
tenant_id UUID NOT NULL - FK a tenants
parent_id UUID NULL - Centro padre
code VARCHAR(20) NOT NULL - Codigo
name VARCHAR(255) NOT NULL - Nombre
level INTEGER NOT NULL 1 Nivel
is_active BOOLEAN NOT NULL true Activo
created_at TIMESTAMPTZ NOT NULL NOW() Fecha creacion
CREATE TABLE core_financial.cost_centers (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    tenant_id UUID NOT NULL,
    parent_id UUID REFERENCES core_financial.cost_centers(id),
    code VARCHAR(20) NOT NULL,
    name VARCHAR(255) NOT NULL,
    level INTEGER NOT NULL DEFAULT 1,
    is_active BOOLEAN NOT NULL DEFAULT true,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),

    CONSTRAINT uk_cost_centers_tenant_code UNIQUE (tenant_id, code)
);

-- RLS
ALTER TABLE core_financial.cost_centers ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON core_financial.cost_centers
    FOR ALL USING (tenant_id = current_setting('app.current_tenant_id')::uuid);

9. journal_entries

Asientos contables (cabecera).

Columna Tipo Nullable Default Descripcion
id UUID NOT NULL gen_random_uuid() PK
tenant_id UUID NOT NULL - FK a tenants
fiscal_period_id UUID NOT NULL - FK a fiscal_periods
entry_number VARCHAR(20) NOT NULL - Numero secuencial
entry_date DATE NOT NULL - Fecha contable
currency_code VARCHAR(3) NOT NULL - Moneda
exchange_rate DECIMAL(18,8) NOT NULL 1 Tipo cambio a base
reference VARCHAR(100) NULL - Referencia externa
description TEXT NOT NULL - Descripcion
source_module VARCHAR(50) NULL - Modulo origen
source_document_id UUID NULL - Documento origen
status VARCHAR(20) NOT NULL 'draft' Estado
total_debit DECIMAL(18,4) NOT NULL 0 Total debito
total_credit DECIMAL(18,4) NOT NULL 0 Total credito
posted_at TIMESTAMPTZ NULL - Fecha contabilizado
posted_by UUID NULL - Usuario que contabilizo
reversed_by UUID NULL - Asiento reverso
created_by UUID NOT NULL - Usuario creador
created_at TIMESTAMPTZ NOT NULL NOW() Fecha creacion
updated_at TIMESTAMPTZ NOT NULL NOW() Fecha actualizacion
CREATE TABLE core_financial.journal_entries (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    tenant_id UUID NOT NULL,
    fiscal_period_id UUID NOT NULL REFERENCES core_financial.fiscal_periods(id),
    entry_number VARCHAR(20) NOT NULL,
    entry_date DATE NOT NULL,
    currency_code VARCHAR(3) NOT NULL,
    exchange_rate DECIMAL(18,8) NOT NULL DEFAULT 1,
    reference VARCHAR(100),
    description TEXT NOT NULL,
    source_module VARCHAR(50),
    source_document_id UUID,
    status VARCHAR(20) NOT NULL DEFAULT 'draft',
    total_debit DECIMAL(18,4) NOT NULL DEFAULT 0,
    total_credit DECIMAL(18,4) NOT NULL DEFAULT 0,
    posted_at TIMESTAMPTZ,
    posted_by UUID,
    reversed_by UUID REFERENCES core_financial.journal_entries(id),
    created_by UUID NOT NULL,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),

    CONSTRAINT uk_journal_entries_number UNIQUE (tenant_id, entry_number),
    CONSTRAINT chk_journal_status CHECK (status IN ('draft', 'posted', 'reversed')),
    CONSTRAINT chk_journal_balance CHECK (
        status = 'draft' OR (total_debit = total_credit AND total_debit > 0)
    )
);

CREATE INDEX idx_journal_entries_period ON core_financial.journal_entries(fiscal_period_id);
CREATE INDEX idx_journal_entries_date ON core_financial.journal_entries(tenant_id, entry_date);
CREATE INDEX idx_journal_entries_status ON core_financial.journal_entries(status);
CREATE INDEX idx_journal_entries_source ON core_financial.journal_entries(source_module, source_document_id);

-- RLS
ALTER TABLE core_financial.journal_entries ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON core_financial.journal_entries
    FOR ALL USING (tenant_id = current_setting('app.current_tenant_id')::uuid);

10. journal_lines

Lineas de asiento (partida doble).

Columna Tipo Nullable Default Descripcion
id UUID NOT NULL gen_random_uuid() PK
journal_entry_id UUID NOT NULL - FK a journal_entries
line_number INTEGER NOT NULL - Numero de linea
account_id UUID NOT NULL - FK a accounts
cost_center_id UUID NULL - FK a cost_centers
debit DECIMAL(18,4) NOT NULL 0 Monto debito (moneda doc)
credit DECIMAL(18,4) NOT NULL 0 Monto credito (moneda doc)
debit_base DECIMAL(18,4) NOT NULL 0 Debito en moneda base
credit_base DECIMAL(18,4) NOT NULL 0 Credito en moneda base
description TEXT NULL - Descripcion linea
reference VARCHAR(100) NULL - Referencia
CREATE TABLE core_financial.journal_lines (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    journal_entry_id UUID NOT NULL REFERENCES core_financial.journal_entries(id) ON DELETE CASCADE,
    line_number INTEGER NOT NULL,
    account_id UUID NOT NULL REFERENCES core_financial.accounts(id),
    cost_center_id UUID REFERENCES core_financial.cost_centers(id),
    debit DECIMAL(18,4) NOT NULL DEFAULT 0,
    credit DECIMAL(18,4) NOT NULL DEFAULT 0,
    debit_base DECIMAL(18,4) NOT NULL DEFAULT 0,
    credit_base DECIMAL(18,4) NOT NULL DEFAULT 0,
    description TEXT,
    reference VARCHAR(100),

    CONSTRAINT uk_journal_lines_entry_line UNIQUE (journal_entry_id, line_number),
    CONSTRAINT chk_journal_lines_amounts CHECK (
        (debit > 0 AND credit = 0) OR (debit = 0 AND credit > 0) OR (debit = 0 AND credit = 0)
    )
);

CREATE INDEX idx_journal_lines_entry ON core_financial.journal_lines(journal_entry_id);
CREATE INDEX idx_journal_lines_account ON core_financial.journal_lines(account_id);
CREATE INDEX idx_journal_lines_cost_center ON core_financial.journal_lines(cost_center_id);

Funciones de Utilidad

Obtener Tipo de Cambio

CREATE OR REPLACE FUNCTION core_financial.get_exchange_rate(
    p_tenant_id UUID,
    p_from_currency VARCHAR,
    p_to_currency VARCHAR,
    p_date DATE DEFAULT CURRENT_DATE
) RETURNS DECIMAL(18,8) AS $$
DECLARE
    v_rate DECIMAL(18,8);
BEGIN
    IF p_from_currency = p_to_currency THEN
        RETURN 1;
    END IF;

    -- Buscar tasa directa
    SELECT rate INTO v_rate
    FROM core_financial.exchange_rates
    WHERE tenant_id = p_tenant_id
    AND from_currency = p_from_currency
    AND to_currency = p_to_currency
    AND effective_date <= p_date
    ORDER BY effective_date DESC
    LIMIT 1;

    IF v_rate IS NOT NULL THEN
        RETURN v_rate;
    END IF;

    -- Buscar tasa inversa
    SELECT 1.0 / rate INTO v_rate
    FROM core_financial.exchange_rates
    WHERE tenant_id = p_tenant_id
    AND from_currency = p_to_currency
    AND to_currency = p_from_currency
    AND effective_date <= p_date
    ORDER BY effective_date DESC
    LIMIT 1;

    IF v_rate IS NOT NULL THEN
        RETURN v_rate;
    END IF;

    RAISE EXCEPTION 'Exchange rate not found for % to % on %', p_from_currency, p_to_currency, p_date;
END;
$$ LANGUAGE plpgsql STABLE;

Obtener Periodo para Fecha

CREATE OR REPLACE FUNCTION core_financial.get_period_for_date(
    p_tenant_id UUID,
    p_date DATE
) RETURNS UUID AS $$
DECLARE
    v_period_id UUID;
BEGIN
    SELECT id INTO v_period_id
    FROM core_financial.fiscal_periods
    WHERE tenant_id = p_tenant_id
    AND p_date BETWEEN start_date AND end_date
    AND status = 'open'
    LIMIT 1;

    IF v_period_id IS NULL THEN
        RAISE EXCEPTION 'No open period found for date %', p_date;
    END IF;

    RETURN v_period_id;
END;
$$ LANGUAGE plpgsql STABLE;

Contabilizar Asiento

CREATE OR REPLACE FUNCTION core_financial.post_journal_entry(
    p_entry_id UUID,
    p_user_id UUID
) RETURNS BOOLEAN AS $$
DECLARE
    v_entry RECORD;
    v_line RECORD;
    v_period_status VARCHAR;
BEGIN
    -- Obtener asiento
    SELECT * INTO v_entry
    FROM core_financial.journal_entries
    WHERE id = p_entry_id FOR UPDATE;

    IF v_entry IS NULL THEN
        RAISE EXCEPTION 'Journal entry not found';
    END IF;

    IF v_entry.status != 'draft' THEN
        RAISE EXCEPTION 'Entry is not in draft status';
    END IF;

    -- Verificar periodo abierto
    SELECT status INTO v_period_status
    FROM core_financial.fiscal_periods
    WHERE id = v_entry.fiscal_period_id;

    IF v_period_status != 'open' THEN
        RAISE EXCEPTION 'Fiscal period is not open';
    END IF;

    -- Verificar balance
    IF v_entry.total_debit != v_entry.total_credit THEN
        RAISE EXCEPTION 'Entry is not balanced: debit=% credit=%',
            v_entry.total_debit, v_entry.total_credit;
    END IF;

    -- Actualizar saldos de cuentas
    FOR v_line IN
        SELECT jl.*, a.current_balance, at.normal_balance
        FROM core_financial.journal_lines jl
        JOIN core_financial.accounts a ON a.id = jl.account_id
        JOIN core_financial.account_types at ON at.id = a.account_type_id
        WHERE jl.journal_entry_id = p_entry_id
    LOOP
        UPDATE core_financial.accounts
        SET current_balance = current_balance +
            CASE WHEN v_line.normal_balance = 'debit'
                THEN v_line.debit_base - v_line.credit_base
                ELSE v_line.credit_base - v_line.debit_base
            END,
            updated_at = NOW()
        WHERE id = v_line.account_id;
    END LOOP;

    -- Marcar como contabilizado
    UPDATE core_financial.journal_entries
    SET status = 'posted',
        posted_at = NOW(),
        posted_by = p_user_id
    WHERE id = p_entry_id;

    RETURN true;
END;
$$ LANGUAGE plpgsql;

Obtener Saldo de Cuenta por Periodo

CREATE OR REPLACE FUNCTION core_financial.get_account_balance(
    p_account_id UUID,
    p_as_of_date DATE DEFAULT NULL
) RETURNS TABLE (
    debit_total DECIMAL(18,4),
    credit_total DECIMAL(18,4),
    balance DECIMAL(18,4)
) AS $$
BEGIN
    RETURN QUERY
    SELECT
        COALESCE(SUM(jl.debit_base), 0) as debit_total,
        COALESCE(SUM(jl.credit_base), 0) as credit_total,
        COALESCE(SUM(jl.debit_base - jl.credit_base), 0) as balance
    FROM core_financial.journal_lines jl
    JOIN core_financial.journal_entries je ON je.id = jl.journal_entry_id
    WHERE jl.account_id = p_account_id
    AND je.status = 'posted'
    AND (p_as_of_date IS NULL OR je.entry_date <= p_as_of_date);
END;
$$ LANGUAGE plpgsql STABLE;

Seed Data

Crear Plan de Cuentas Base

CREATE OR REPLACE FUNCTION core_financial.create_default_chart(
    p_tenant_id UUID
) RETURNS UUID AS $$
DECLARE
    v_chart_id UUID;
    v_type_id UUID;
BEGIN
    -- Crear chart
    INSERT INTO core_financial.charts_of_accounts (tenant_id, code, name)
    VALUES (p_tenant_id, 'PRINCIPAL', 'Plan de Cuentas Principal')
    RETURNING id INTO v_chart_id;

    -- Cuentas principales
    -- Activos
    SELECT id INTO v_type_id FROM core_financial.account_types WHERE code = 'ASSET_CURRENT';
    INSERT INTO core_financial.accounts (chart_id, tenant_id, account_type_id, code, name, level, is_detail)
    VALUES
    (v_chart_id, p_tenant_id, v_type_id, '1', 'ACTIVO', 1, false),
    (v_chart_id, p_tenant_id, v_type_id, '1.1', 'Activo Circulante', 2, false),
    (v_chart_id, p_tenant_id, v_type_id, '1.1.1', 'Caja y Bancos', 3, false),
    (v_chart_id, p_tenant_id, v_type_id, '1.1.1.1', 'Caja General', 4, true),
    (v_chart_id, p_tenant_id, v_type_id, '1.1.1.2', 'Bancos', 4, true),
    (v_chart_id, p_tenant_id, v_type_id, '1.1.2', 'Cuentas por Cobrar', 3, true);

    -- Pasivos
    SELECT id INTO v_type_id FROM core_financial.account_types WHERE code = 'LIABILITY_CURRENT';
    INSERT INTO core_financial.accounts (chart_id, tenant_id, account_type_id, code, name, level, is_detail)
    VALUES
    (v_chart_id, p_tenant_id, v_type_id, '2', 'PASIVO', 1, false),
    (v_chart_id, p_tenant_id, v_type_id, '2.1', 'Pasivo Circulante', 2, false),
    (v_chart_id, p_tenant_id, v_type_id, '2.1.1', 'Cuentas por Pagar', 3, true);

    -- Capital
    SELECT id INTO v_type_id FROM core_financial.account_types WHERE code = 'EQUITY';
    INSERT INTO core_financial.accounts (chart_id, tenant_id, account_type_id, code, name, level, is_detail)
    VALUES
    (v_chart_id, p_tenant_id, v_type_id, '3', 'CAPITAL', 1, false),
    (v_chart_id, p_tenant_id, v_type_id, '3.1', 'Capital Social', 2, true);

    -- Ingresos
    SELECT id INTO v_type_id FROM core_financial.account_types WHERE code = 'REVENUE';
    INSERT INTO core_financial.accounts (chart_id, tenant_id, account_type_id, code, name, level, is_detail)
    VALUES
    (v_chart_id, p_tenant_id, v_type_id, '4', 'INGRESOS', 1, false),
    (v_chart_id, p_tenant_id, v_type_id, '4.1', 'Ingresos Operativos', 2, true);

    -- Gastos
    SELECT id INTO v_type_id FROM core_financial.account_types WHERE code = 'EXPENSE';
    INSERT INTO core_financial.accounts (chart_id, tenant_id, account_type_id, code, name, level, is_detail)
    VALUES
    (v_chart_id, p_tenant_id, v_type_id, '5', 'GASTOS', 1, false),
    (v_chart_id, p_tenant_id, v_type_id, '5.1', 'Gastos Operativos', 2, true);

    RETURN v_chart_id;
END;
$$ LANGUAGE plpgsql;

Historial

Version Fecha Autor Cambios
1.0 2025-12-05 Requirements-Analyst Creacion inicial