Plataforma técnica · Orchestrator
Resultados Acumulados Service
Orchestrator Reportes Resultados Acumulados
ResultadosAcumuladosService materializa el resultado del ejercicio que se traspasa a patrimonio. Soporta dos lecturas paralelas:
- FINANCIERO — desde el balance (cuentas de resultado del plan IFRS/tributario consolidado).
- TRIBUTARIO — desde la base imponible (renta).
Ambas conviven porque el resultado financiero (NIC 1) y el resultado tributario (RLI) suelen diferir por agregados, deducciones extraordinarias y diferencias temporarias/permanentes.
Extiende ReportesServiceSupport para reusar this.calcular(...).
Métodos
Section titled “Métodos”| Método | Propósito |
|---|---|
getResultadosAcumulados(ctx, filtros?) | Lista filas. Filtros opcionales: anio, tipo_resultado. |
generarResultadoAcumulado(ctx, dto) | Computa y upserta una fila para (anio, tipo_resultado). |
actualizarEstadoResultadoAcumulado(ctx, id, dto) | Transiciona estado. Opcional: actualiza notas. |
generarResultadoAcumulado
Section titled “generarResultadoAcumulado”async generarResultadoAcumulado( ctx: ServiceContext, data: GenerarResultadoAcumuladoDTO,): Promise<ServiceResult<ResultadoAcumuladoRow>>
interface GenerarResultadoAcumuladoDTO { anio: number; tipo_resultado: "FINANCIERO" | "TRIBUTARIO"; tipo_presentacion?: "TRIBUTARIO" | "IFRS"; // default "TRIBUTARIO" modo_periodo?: "MENSUAL" | "ANUAL_ACUMULADO"; // ignorado hoy mes?: number; // default 12 notas?: string | null;}flowchart TB
IN["generarResultadoAcumulado(dto)"]
VAL["validate anio/tipo_resultado/tipo_presentacion/mes"]
CALC["this.calcular(ctx, anio, tipo_presentacion, {mes})<br/>→ ReporteAnual completo"]
BIF{"tipo_resultado"}
TRIB["resultado = reporte.renta.base_imponible<br/>detalle = { fuente: 'BASE_IMPONIBLE', ... }"]
FIN["resultado = SUM(nivel4 ganancias - perdidas)<br/>detalle = { fuente: 'BALANCE_CUENTAS', cuadra, ... }"]
UP["upsertResultadoAcumulado(...)<br/>(anio, tipo_resultado) PK lógica"]
OUT["ResultadoAcumuladoRow"]
IN --> VAL --> CALC --> BIF
BIF -- TRIBUTARIO --> TRIB --> UP
BIF -- FINANCIERO --> FIN --> UP
UP --> OUT Resultado FINANCIERO
Section titled “Resultado FINANCIERO”const nivel4 = reporte.lineas_balance.filter((l) => l.nivel === 4);const totalActivoPasivo = nivel4.reduce( (acc, l) => acc + Number(l.activo ?? 0) - Number(l.pasivo ?? 0), 0,);const totalIngresosGastos = nivel4.reduce( (acc, l) => acc + Number(l.ganancias ?? 0) - Number(l.perdidas ?? 0), 0,);resultadoPeriodo = totalIngresosGastos;detalle = { fuente: "BALANCE_CUENTAS", resultado_activo_pasivo: totalActivoPasivo, resultado_ingresos_gastos: totalIngresosGastos, resultado_ejercicio: reporte.estado_resultados.resultado_ejercicio, diferencia: totalActivoPasivo - totalIngresosGastos, cuadra: Math.round(Math.abs(totalActivoPasivo - totalIngresosGastos)) === 0,};El doble cómputo (activo-pasivo vs ingresos-gastos) verifica que el balance cuadra por ambas vías. Si cuadra: false, hay líneas faltantes o cuentas mal clasificadas — la fila se persiste igual con la flag para auditoría.
Resultado TRIBUTARIO
Section titled “Resultado TRIBUTARIO”resultadoPeriodo = Number(reporte.renta.base_imponible ?? 0);detalle = { fuente: "BASE_IMPONIBLE", base_imponible: reporte.renta.base_imponible, total_ingresos: reporte.renta.total_ingresos, total_deducciones: reporte.renta.total_deducciones, ppm: reporte.renta.ppm, impuesto_estimado: reporte.renta.impuesto_estimado, diferencia: reporte.renta.diferencia,};Toma la base imponible tal cual (no la divide por la tasa, no le aplica créditos). Es el resultado tributario antes de imputaciones que después la RLI ajustará.
Persistencia
Section titled “Persistencia”reportes.resultados_acumulados:
| Columna | Tipo | Notas |
|---|---|---|
id | bigserial | PK |
anio | int | Año del ejercicio |
tipo_resultado | text | FINANCIERO o TRIBUTARIO |
tipo_presentacion | text | TRIBUTARIO o IFRS (para el caso FINANCIERO importa porque cambia los nombres; numéricamente son iguales) |
cuenta_codigo | text | 2302001 (utilidades acumuladas) o 2302002 (pérdidas acumuladas) según signo |
resultado_periodo | numeric | Resultado del año cerrado |
acumulado_anterior | numeric | Resultado acumulado al inicio del año |
acumulado_actual | numeric | acumulado_anterior + resultado_periodo |
estado | text | BORRADOR, CONTABILIZADO, APROBADO |
balance_guardado_id | uuid | FK opcional al snapshot de balance que originó la fila |
detalle | jsonb | Trazabilidad del cómputo (fuente, sub-totales, diferencia, cuadra) |
notas | text | Comentarios del usuario |
fecha_resultado | date | YYYY-MM-01 del cierre |
fecha_contabilizacion | timestamp | Cuándo se transicionó a CONTABILIZADO |
Upsert por (anio, tipo_resultado) — regenerar un resultado del mismo tipo sobreescribe la fila anterior.
Lifecycle
Section titled “Lifecycle”stateDiagram-v2 [*] --> BORRADOR : generar BORRADOR --> CONTABILIZADO : actualizarEstado(CONTABILIZADO) CONTABILIZADO --> APROBADO : actualizarEstado(APROBADO) APROBADO --> CONTABILIZADO : actualizarEstado(CONTABILIZADO) CONTABILIZADO --> BORRADOR : actualizarEstado(BORRADOR)
async actualizarEstadoResultadoAcumulado( ctx: ServiceContext, id: number, data: ActualizarResultadoAcumuladoEstadoDTO,): Promise<ServiceResult<ResultadoAcumuladoRow>>Validación:
const estado = String(data.estado ?? "").trim().toUpperCase();if (!["BORRADOR", "CONTABILIZADO", "APROBADO"].includes(estado)) { throw new ValidationError("estado inválido. Use BORRADOR, CONTABILIZADO o APROBADO");}No hay guards de transición — cualquier transición entre los 3 estados es legal. La razón: el cierre patrimonial admite ir y volver durante el período de revisión anual; el usuario decide el orden.
Update opcional de notas:
const hasNotas = Object.prototype.hasOwnProperty.call(data, "notas");Si el DTO incluye la propiedad notas (aún con valor null), se actualiza. Si no la incluye, se preserva. Esto permite cambiar solo el estado sin tocar las notas existentes.
getResultadosAcumulados
Section titled “getResultadosAcumulados”async getResultadosAcumulados( ctx: ServiceContext, filtros?: FiltrosResultadoAcumulado,): Promise<ServiceResult<ResultadoAcumuladoRow[]>>
interface FiltrosResultadoAcumulado { anio?: number; tipo_resultado?: "FINANCIERO" | "TRIBUTARIO";}Lista directamente desde el repository. Sin filtros retorna todas las filas históricas del tenant.
Conexión con el ciclo contable anual
Section titled “Conexión con el ciclo contable anual”CicloContableService.getEstadoAnual paso resultados_acumulados delega a ReportesRepository.getEstadoResultadosAcumuladosAnual(pool, anio) que retorna financiero_ok y tributario_ok (booleans). El estado completo exige ambos OK — coherente con que la convención es generar las dos lecturas y compararlas.
| Estado | Condición | Lectura |
|---|---|---|
pendiente | No hay registros del año | Aún no se ejecutó el cierre patrimonial |
parcial | Solo una de las dos lecturas (financiero o tributario) | Falta generar el contraparte |
completo | Ambas lecturas existen y están al menos CONTABILIZADO | Listo para arrastrar al año siguiente |
Cuentas patrimoniales
Section titled “Cuentas patrimoniales”| Código | Cuenta |
|---|---|
2302001 | Utilidades acumuladas |
2302002 | Pérdidas acumuladas |
cuenta_codigo se persiste según el signo de resultado_periodo: positivo → 2302001, negativo → 2302002. Esto facilita auditar el asiento de cierre (resultado del ejercicio → patrimonio) sin ambigüedad.
Por qué tipo_presentacion también en el DTO
Section titled “Por qué tipo_presentacion también en el DTO”Aunque el resultado numérico FINANCIERO no cambia entre TRIBUTARIO e IFRS (mismas líneas, mismos saldos), tipo_presentacion se persiste porque:
- La trazabilidad del cómputo guarda nombres de cuentas que sí cambian con IFRS.
- Una empresa puede generar ambas vistas para presentación externa.
- El campo no compone PK lógica (
(anio, tipo_resultado)la define), pero queda como metadata informativa.