-- ============================================================= -- 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)';