Skip to content

Plataforma técnica · Orchestrator

Reportes Service Support

Orchestrator Reportes

ReportesServiceSupport es una clase abstracta que contiene el motor de presentación del dominio reportes. No se instancia directamente; los 5 services del dominio (BalanceService, CierrePeriodoService, PpmService, RentaService, ResultadosAcumuladosService) la extienden para compartir la misma proyección y evitar drift entre cálculos hechos en diferentes contextos.

Extiende BaseService y compone CommonDataService para acceso a parámetros (UF, UTM, factores de corrección monetaria).

export abstract class ReportesServiceSupport extends BaseService {
protected readonly commonDataService: CommonDataService;
constructor(serviceName: string) { ... }
// Entry point reutilizable (mismo cuerpo que BalanceService.calcular)
async calcular(ctx, anio, tipo, periodo): Promise<ServiceResult<ReporteAnual>>
// El motor
protected buildBalanceClasificado(lineas: BalanceLinea[]): BalanceClasificado
protected buildEstadoResultados(lineas: BalanceLinea[]): EstadoResultados
protected buildRenta(raw): BalanceTributario
protected mapearNombresIfrs(lineas: BalanceLinea[]): BalanceLinea[]
// Helpers PPM
protected buildPpmFactorMap(factoresRaw): Map<...>
protected resolvePpmFactorSourceMonth(anio, declarado): number | null
protected buildPpmActualizacionAnualResumen(anio, declarados, persistidos, factoresRaw)
protected roundPpmAmount(value): number
}

Tres razones convergen:

  1. Composición de serviciosCierrePeriodoService.cerrarPeriodo invoca this.calcular(...) para obtener las líneas a persistir. ResultadosAcumuladosService.generarResultadoAcumulado también invoca this.calcular(...) para resolver el resultado del período. Ambos heredan en lugar de inyectar.
  2. Consistencia obligada — si la lógica de buildBalanceClasificado vive en un helper suelto, alguien puede saltársela y proyectar el balance con su propio loop, divergiendo. Como método protegido de la base, queda fuera del público y no se duplica.
  3. Acceso a commonDataService — los helpers PPM necesitan factores de corrección monetaria. Vivir en la base permite que cada subclase use this.commonDataService sin reinicializar.
flowchart LR
  IN["BalanceLinea[]<br/>(filas nivel 1-4)"]
  BC["buildBalanceClasificado"]
  ER["buildEstadoResultados"]
  RT["buildRenta"]
  OUT1["BalanceClasificado<br/>(presentación A=P+P)"]
  OUT2["EstadoResultados<br/>(EERR)"]
  OUT3["BalanceTributario<br/>(base imponible)"]

  IN --> BC --> OUT1
  IN --> ER --> OUT2
  IN -. usa rentaRaw .-> RT --> OUT3

Filtra solo nivel 3 (subsección) y agrupa por codigo_padre (nivel 2 anclas):

ConstanteCódigo nivel 2Sección
SEC_ACTIVO_CTE1100000Activos corrientes
SEC_ACTIVO_NCTE1200000Activos no corrientes
SEC_PASIVO_CTE2100000Pasivos corrientes
SEC_PASIVO_NCTE2200000Pasivos no corrientes
SEC_PATRIMONIO2300000Patrimonio

Reglas de monto por sección:

SecciónFórmula por líneaPor qué
Activosmonto = activo - pasivoContra-cuentas (depreciación acumulada con tipo=ACTIVO + naturaleza=C) tienen activo=0, pasivo>0 → restan
Pasivosmonto = pasivoPasivos solo informan saldo acreedor
Patrimoniomonto = pasivo - activoPérdidas acumuladas (tipo=PATRIMONIO + naturaleza=D) reducen patrimonio

El resultado del ejercicio se suma como una pseudo-línea de patrimonio:

const resultadoEjercicio = n3.reduce(
(s, l) => s + Number(l.ganancias ?? 0) - Number(l.perdidas ?? 0),
0,
);
// Se inyecta como línea sintética con codigo "RESULTADO_EJERCICIO"

Cierre: total_activos == total_pasivos + total_patrimoniocuadra: true. Tolerancia: redondeo entero por Math.round(Math.abs(...)).

Mismo patrón: filtra nivel 3 por anclas nivel 2 de cuentas de resultado.

ConstanteCódigo nivel 2Bucket EERR
SEC_ING_ORD4100000Ingresos operacionales
SEC_ING_OTROS4200000Ingresos operacionales (otros)
SEC_COSTO3100000Costo de ventas
SEC_ADM3200000Gastos operacionales
SEC_VTA3300000Gastos operacionales
SEC_OTROS_IMP3700000Gastos operacionales (otros impuestos)
SEC_FIN3400000Resultado no operacional
SEC_OTROS_G3500000Resultado no operacional
SEC_IMP_RENTA3600000Impuesto a la renta

Cascada:

total_ingresos = SUM(ingresos.ganancias)
total_costo = SUM(costo_ventas.perdidas)
margen_bruto = total_ingresos - total_costo
total_gastos_op = SUM(gastosOperacionales.perdidas)
resultado_operacional = margen_bruto - total_gastos_op
total_no_op = SUM(gastosNoOp.perdidas)
resultado_antes_impuesto = resultado_operacional - total_no_op
impuesto_renta = gastos en SEC_IMP_RENTA si > 0
:: round(resultado_antes_impuesto * TASA_IMPUESTO) si no hay cuentas
:: 0 si resultado antes de impuesto es negativo
resultado_ejercicio = resultado_antes_impuesto - impuesto_renta

Diferente a los otros: no parte de líneas del libro diario sino de un raw de RentaRepository.calcularRenta (vista SQL que suma ventas, compras, honorarios e impuestos específicos del año).

total_ingresos = ventas_netas + nc_ventas
total_compras = compras_netas + nc_compras
total_deducciones = total_compras + honorarios + impuestos_específicos
base_imponible = total_ingresos - total_deducciones
impuesto_estimado = max(0, base_imponible) * TASA_IMPUESTO
diferencia = impuesto_estimado - ppm

Solo redondea y compone — la heavy lifting vive en SQL.

return lineas.map((l) => ({
...l,
nombre: l.nombre_ifrs ?? l.nombre,
}));

Reemplazo 1-a-1 cuando el plan tiene nombre_ifrs definido. Si no, usa nombre (presentación tributaria).

Encapsulan la actualización de PPM por corrección monetaria al cierre anual. Detalle del cálculo y reglas en PpmService. Los helpers en esta base:

HelperRol
buildPpmFactorMap(factoresRaw)Construye Map<mesOrigen, { factor, fuenteUrl }> filtrando solo factores con mes_destino = 12 (corrección al cierre anual).
resolvePpmFactorSourceMonth(anio, declarado)Decide qué mes de factor aplicar a un PPM declarado: usa fecha_pago si está, sino mes + 1 (el factor aplica desde el mes siguiente a la declaración).
roundPpmAmount(value)Math.round(Number(value ?? 0)) — wrapping para consistencia en montos PPM.
buildPpmActualizacionAnualResumenCompone la tabla 12-mes con ppm_declarado, factor, ppm_actualizado, ajuste_correccion, estado por mes + estado general (PENDIENTE/PARCIAL/CONTABILIZADO).

Método legacy: _aplicarArrastreAnualLegacy

Section titled “Método legacy: _aplicarArrastreAnualLegacy”

Marcado con _ por convención (uso interno transicional). Aplica arrastre de saldo del año anterior cuando el cálculo actual no incluye apertura. Maneja excepciones críticas:

  • IVA F29 (1108002, 2103003) — no se arrastra; tiene lógica propia en el cálculo del período.
  • Existencias (110900*) — workaround temporal; el módulo de inventario trae su propio saldo inicial. Excepción: 1109004 (existencias en tránsito) sí arrastra.
  • PPM por recuperar/pagar (1108001, 2202001) — no descuenta el bruto del año previo en delta.
  • Depreciación acumulada (1202* con naturaleza=C) — caso especial para detectar acumulado vs movimiento.

Hoy no se invoca desde el flujo principal (la actualización por arrastre se hizo desde SQL/saldo_cuentas_cierre). Queda en la clase como referencia y para tests de comportamiento histórico.

Clasificación nivel-4: nivel4Classification.ts

Section titled “Clasificación nivel-4: nivel4Classification.ts”

Función pura externa que el motor usa indirectamente (a través de _aplicarArrastreAnualLegacy y del SQL fn_balance_lineas). Decide si un saldo va a activo o pasivo:

function classifyNivel4Balance({ tipo, naturaleza, debe, haber }): {
saldo_deudor: number;
saldo_acreedor: number;
activo: number;
pasivo: number;
}
Tipo cuentaNaturalezaSaldo va aNotas
ACTIVOD (deudora)activoCaso normal: caja, banco, deudores
ACTIVOC (acreedora)pasivoContra-cuenta: depreciación acumulada, estimación incobrables
PASIVOC (acreedora)pasivoCaso normal: proveedores, deudas tributarias
PATRIMONIOC (acreedora)pasivoCaso normal: capital, utilidades retenidas
PATRIMONIOD (deudora)ambos (activo/pasivo)Contra-cuenta: pérdidas acumuladas (2302002)

Las contra-cuentas conocidas viven enumeradas en INVERTED_NATURE_ACCOUNTS:

CódigoCuenta
1104002Estimación de deudores incobrables
1202000Depreciación acumulada (genérica)
1202100Dep. acum. edificios
1202300Dep. acum. muebles
1202400Dep. acum. vehículos
1202500Dep. acum. maquinaria
1202600Dep. acum. equipos computacionales
1202700Dep. acum. herramientas
2302002Pérdidas acumuladas

Esta lista es la fuente de verdad de cuentas con naturaleza invertida en el plan contable. Si se agrega una contra-cuenta nueva al plan, debe registrarse aquí o la clasificación queda incorrecta.

BalanceService.calcular y ReportesServiceSupport.calcular tienen el mismo cuerpo: validación de input, Promise.all([calcularLineasBalance, calcularRenta]), mapeo IFRS si aplica, construcción de los 3 builders. Está duplicado para:

  • Permitir que CierrePeriodoService.cerrarPeriodo invoque this.calcular(...) sin instanciar BalanceService (evita ciclo de instancias y acoplamiento al lifecycle público).
  • Permitir que ResultadosAcumuladosService.generarResultadoAcumulado haga lo mismo.
  • Mantener BalanceService.calcular como la API estable del dominio expuesta por HTTP.

La alternativa (mover todo a la base y dejar BalanceService.calcular como return super.calcular(...)) es válida pero introduce un nivel extra de indirección sin beneficio claro. La duplicación es intencional y los tests cubren ambas rutas.