Dimensiones Contables
Modelo dimensional SAP-like
A partir del Sub-proyecto 1 — Fundamento Dimensional (mayo 2026), cada línea de un asiento contable (journal_entry_lines) lleva 5 dimensiones independientes que responden 5 preguntas distintas:
| Dimensión | Pregunta que responde | Tabla / FK |
|---|---|---|
| Plan de Cuentas | ¿Qué naturaleza tiene el flujo? (activo, pasivo, gasto, ingreso) | account_id → accounting_accounts |
| Centro de Costo | ¿Quién o qué unidad organizacional es responsable? | cost_center_id → cost_centers |
| Área Funcional | ¿Cómo se reporta a PDVSA? (clasificador upstream) | functional_area_id → functional_areas |
| Proyecto / Pozo | ¿Para qué iniciativa o activo se incurre? | project_id → projects |
| Renglón Presupuestario | ¿Qué partida del presupuesto consume? | budget_position_id → budget_positions |
department_id se conserva como dimensión derivada (a través de cost_center.department_id) y se materializa en cada línea para queries rápidas.
Por qué 5 dimensiones independientes
Antes del Sub-proyecto 1 el sistema mezclaba tres conceptos en una única tabla cost_centers:
- Centro de costo (responsabilidad organizacional)
- Clasificación presupuestaria PDVSA (renglón)
- Mapeo a plan de cuentas
Esto rompe la regla básica de los ERPs serios (SAP, Oracle, Odoo): cada dimensión debe poder filtrarse, sumarse y reportarse de forma independiente. Un mismo gasto puede ser:
- Centro de Costo
CC-OPS-POZOS(responsable) - Área Funcional
LEV(PDVSA) - Proyecto
Pozo Sinco-3(iniciativa) - Renglón
ACE-1-1AL-LEV(partida presupuestaria) - Cuenta
6.3.01.018(materiales)
Si están en una sola tabla, no se pueden cruzar correctamente.
Reglas de Validación en journalEntryService.create()
Al crear una línea de asiento, el backend valida según el tipo de cuenta:
Cuenta EXPENSE → cost_center_id NOT NULL
Cuenta REVENUE → cost_center_id NOT NULL
Cuenta CAPEX → project_id NOT NULL
(CAPEX = subtipo FIXED_ASSET o INTANGIBLE_ASSET)
budget_position_id presente
→ debe existir un link en account_budget_position_links
para esa cuenta con role IN (DEBIT_DEFAULT, CREDIT_DEFAULT, ALLOWED)
functional_area_id ausente + budget_position_id presente
→ se infiere automáticamente desde bp.functional_area_id
Ejemplos válidos
// Salario imputado a centro de costo OPS, área PER, sin proyecto
{
account_id: '6.1.01.001', // Sueldos
cost_center_id: 'CC-OPS',
functional_area_id: 'PER',
budget_position_id: 'ACE-OPS-PER',
debit: 100000
}
// CAPEX de pozo nuevo: project_id obligatorio
{
account_id: '1.2.05.003', // Activo en construcción
cost_center_id: 'CC-PROD',
functional_area_id: 'INPR',
project_id: 'Pozo Sinco-3',
budget_position_id: 'ACE-CAPEX-INPR',
debit: 50000000
}
// Si solo se envía budget_position_id, fa se infiere
{
account_id: '6.3.01.018',
cost_center_id: 'CC-MTO',
budget_position_id: 'ACE-2-2MC-LEV', // bp.functional_area_id = 'LEV'
debit: 250000
// → se rellena automáticamente functional_area_id = 'LEV'
}
Ejemplos rechazados
// ✗ Cuenta EXPENSE sin cost_center
{ account_id: '6.1.01.001', debit: 100 }
// → Error: cost_center_id requerido para cuenta EXPENSE
// ✗ Cuenta CAPEX sin project_id
{ account_id: '1.2.05.003', cost_center_id: 'CC-PROD', debit: 100 }
// → Error: project_id requerido para cuenta CAPEX (FIXED_ASSET)
// ✗ budget_position sin link configurado
{
account_id: '6.1.01.001',
cost_center_id: 'CC-OPS',
budget_position_id: 'ACE-CAPEX-INPR', // CAPEX vs cuenta OPEX
debit: 100
}
// → Error: no existe link en account_budget_position_links para esa cuenta
La tabla puente: account_budget_position_links
Reemplazó al JSONB cost_centers.accounting_accounts. Permite mapear N cuentas ↔ N renglones con un rol semántico.
| Campo | Descripción |
|---|---|
account_id | Cuenta del plan |
budget_position_id | Renglón presupuestario |
role | DEBIT_DEFAULT / CREDIT_DEFAULT / ALLOWED |
is_active | Permite desactivar mapeos sin borrar |
Reglas:
- Sólo un
DEBIT_DEFAULTpor cuenta. - Sólo un
CREDIT_DEFAULTpor cuenta. - Múltiples
ALLOWEDpor cuenta (whitelist amplia).
API: GET|POST|DELETE /api/accounting/account-position-links y consultas inversas GET /api/accounting/accounts/:id/positions y GET /api/accounting/positions/:id/accounts.
Dimensiones propagadas a tablas operativas
Las mismas 5 FKs aparecen en módulos aguas arriba del libro mayor para garantizar que cuando el outbox materializa el asiento, todas las dimensiones llegan completas:
| Tabla | FKs agregadas |
|---|---|
budget_lines | cost_center_id, functional_area_id, budget_position_id |
budget_commitments | cost_center_id, functional_area_id, budget_position_id |
afe_expenses | cost_center_id, functional_area_id, budget_position_id |
journal_entry_lines | las 5 + department_id derivado |
Cada servicio (budgetLineService, purchaseOrderService, materialRequisitionService, payrollService, inventoryAccountingService, …) propaga las dimensiones desde su entidad origen al asiento generado.
Antipatterns evitados
| Antipattern | Síntoma | Solución actual |
|---|---|---|
Mezclar 3 conceptos en cost_centers | Reportes que cruzan responsabilidad con PDVSA producen totales inflados | Tabla cost_centers (responsabilidad), functional_areas (PDVSA) y budget_positions (renglón) separadas |
| Mapear cuentas dentro del CC en JSONB | Imposible hacer JOIN o índice; no se puede consultar "todas las cuentas que tocan el renglón X" | Tabla relacional account_budget_position_links con índices |
Referencia circular Department ↔ CostCenter | Bug clase: el ORM no podía cargar ambos lados | cost_centers.department_id (FK NOT NULL) — departments ya no apunta de vuelta |
Columna string cost_center (DEPRECATED) coexistiendo con FK cost_center_id (UUID) | Doble fuente de verdad | M9 eliminó las columnas string; sólo queda la FK UUID |
Inferir presupuesto desde cost_centers.budget_jan/feb/... | Mensual hardcoded; no soporta moneda dual ni reexpresión NIIF | Presupuesto vive en budget_lines.amount_* con período abierto |
Permisos relacionados
accounting:functional-areas:read | :write
accounting:cost-centers:read | :write (nuevo CC, distinto de budget:positions)
accounting:mappings:read | :write (account_budget_position_links)
budget:positions:read | :write (renombre del cost-centers viejo)
accounting:cost-centers:* controla los 22 CCs reales (responsabilidad). budget:positions:* controla los 149 renglones presupuestarios.
Documentación relacionada
- Áreas Funcionales — clasificador PDVSA
- Renglones Presupuestarios — partidas presupuestarias
- Centros de Costo — unidades organizacionales
- Asientos Contables — registro con partida doble
- Plan de Cuentas — catálogo jerárquico
- Mapeos Contables — integraciones automáticas
Referencia técnica completa: backend_erp/docs/superpowers/specs/2026-05-01-sub1-fundamento-dimensional-design.md.