Skip to content

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étodoPropó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.
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
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.

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á.

reportes.resultados_acumulados:

ColumnaTipoNotas
idbigserialPK
aniointAño del ejercicio
tipo_resultadotextFINANCIERO o TRIBUTARIO
tipo_presentaciontextTRIBUTARIO o IFRS (para el caso FINANCIERO importa porque cambia los nombres; numéricamente son iguales)
cuenta_codigotext2302001 (utilidades acumuladas) o 2302002 (pérdidas acumuladas) según signo
resultado_periodonumericResultado del año cerrado
acumulado_anteriornumericResultado acumulado al inicio del año
acumulado_actualnumericacumulado_anterior + resultado_periodo
estadotextBORRADOR, CONTABILIZADO, APROBADO
balance_guardado_iduuidFK opcional al snapshot de balance que originó la fila
detallejsonbTrazabilidad del cómputo (fuente, sub-totales, diferencia, cuadra)
notastextComentarios del usuario
fecha_resultadodateYYYY-MM-01 del cierre
fecha_contabilizaciontimestampCuándo se transicionó a CONTABILIZADO

Upsert por (anio, tipo_resultado) — regenerar un resultado del mismo tipo sobreescribe la fila anterior.

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.

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.

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.

EstadoCondiciónLectura
pendienteNo hay registros del añoAún no se ejecutó el cierre patrimonial
parcialSolo una de las dos lecturas (financiero o tributario)Falta generar el contraparte
completoAmbas lecturas existen y están al menos CONTABILIZADOListo para arrastrar al año siguiente
CódigoCuenta
2302001Utilidades acumuladas
2302002Pé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:

  1. La trazabilidad del cómputo guarda nombres de cuentas que sí cambian con IFRS.
  2. Una empresa puede generar ambas vistas para presentación externa.
  3. El campo no compone PK lógica ((anio, tipo_resultado) la define), pero queda como metadata informativa.