Contabilidad y declaraciones (App A)Cálculos (libros, D-153, IVA)

Cálculos (Contabilidad)

El núcleo de App A es transformar los XML en filas de libro con su desglose de IVA, y de ahí armar las declaraciones. Todo el cálculo es client-side.

Tarifas de IVA — lib/accountants/rateConfig.ts

RATE_CONFIG define 6 tarifas con { id, label, ivaKey, percent, schemaSuffix }:

id%ivaKey
rate_0_50.005IVA(0.5%)
rate_10.01IVA(1%)
rate_20.02IVA(2%)
rate_30.03IVA(3%)
rate_40.04IVA(4%)
rate_130.13IVA(13%)

Mapeos de códigos de Hacienda en lib/constants/invoiceCodes.ts:

  • CODIGO_TARIFA_IVA — código → etiqueta (08IVA(13%), 02IVA(1%), 09IVA(0.5%), 10IVA(Exenta), 11IVA(0% sin credito), …).
  • CODIGO_NUMERO_TARIFA_IVA — código → tarifa numérica (para detectar mismatch).
  • CODIGO_IMPUESTO_SIGLAS01→IVA, 02→ISC, …

Desglose de IVA por factura

1) Extracción desde el XML — BillsView.flatten()

Por cada LineaDetalle.Impuesto:

  • monto = Impuesto.Monto; impuestoNeto = Impuesto.ImpuestoNeto ?? Linea.ImpuestoNeto ?? Monto.
  • Si el CodigoTarifaIVA es válido (≠ 99): compara la tarifa esperada (CODIGO_NUMERO_TARIFA_IVA[code]) con la tarifa real (Impuesto.Tarifa):
    • Coinciden → acumula en ivaPorTarifa[key] y ivaPorTarifaNeto[key].
    • No coinciden + GASTO → usa la tarifa real para el bucket, marca tarifaMismatch = true y guarda tarifaMismatchDetails { resolvedKey, originalCode, actualTarifa, monto, montoNeto, lineaBase }.
    • No coinciden + VENTA (o sin bucket) → va a TE (impuesto de tarifa especial): teIva += monto, teBase += linea.MontoTotal.
  • Codigo === "99"Otros.

Se escriben en el doc las claves planas IVA(x%), NETO_IVA(x%), Otros, TE, totalTE, y el flag/detalle de mismatch.

2) Filas de libro — bookRowBuilders.ts

buildPurchaseBookRows / buildSalesBookRows filtran por tipoMovimiento y MH === "Aceptado" || MR === "Aceptado". Por fila:

vatAmount = rawVat − returnedVat          // prefiere NETO_*, si no IVA(*)
taxableSubtotal = Σ (ivaValue / rate.percent)   // computeTaxableFromBreakdown (percent>0)
totalVentaNeta  = ResumenFactura.TotalVentaNeta
exemptSubtotal  = (|totalVentaNeta − taxableSubtotal| < 2) ? 0 : (totalVentaNeta − taxableSubtotal)

La tolerancia de 2 CRC absorbe redondeos: si el gravado calculado cuadra con TotalVentaNeta dentro de ±2, el exento es 0.

Notas de crédito (tipo === "Nota de crédito electrónica"): sign = −1 → todos los montos (taxable, exempt, vat, totales, TE, breakdown) se multiplican por −1 → quedan negativos.

groupRowsByPeriod agrupa por periodo (YYYY-MM); summarizeBookRows totaliza.

Overrides de tarifa-mismatch (redistribución, exactamente una vez)

Cuando el código de tarifa del XML no coincide con la tarifa real, el contador decide cómo clasificarlo. La elección se guarda sin redistribuir; la redistribución se aplica en runtime (evita doble conteo al recargar).

  • Detección: en flatten() (ingesta). Se persiste el desglose resuelto a la tarifa real + tarifaMismatch + tarifaMismatchDetails. La elección se guarda como mismatchOverride"tarifa" | "codigo" | "te".
  • Aplicación: LibrosTab.applyMismatchOverridesToRows() (memoizada en displayRows):
    • "tarifa" → no toca el desglose (ya está en la tarifa real); solo recalcula taxable/exempt.
    • "codigo" → resta monto/montoNeto del bucket real y los suma al bucket del código original (CODIGO_TARIFA_IVA[originalCode]).
    • "te" → resta del bucket real y suma a te + totalTe.
    • Limpia buckets con |valor| < 0.01, recalcula vatAmount y taxable/exempt.
  • UI: TarifaMismatchModal (select por fila). Al recargar un libro guardado, el override se rehidrata desde row.mismatchOverride.

Garantía de “una sola vez”: la redistribución NO se persiste; solo se aplica sobre el desglose original en el render. Recargar nunca duplica.

Declaraciones

Se calculan en runtime (no se persisten) y se exportan a PDF/Excel. El formulario se elige por régimen (LibrosTab.formOptions):

RégimenFormularioPeriodicidad
TradicionalD-150Mensual
SimplificadoD-153 (si hay factor IVA), D-104 (físico) o D-105 (jurídico)Trimestral
Especial agropecuarioD-152 (anual) o D-151 (cuatrimestral)

D-150 / D-104 IVA — FormHelpers.ts + formFieldMaps.ts

computeVentasTotalsFromRows / computeComprasTotalsFromRows producen FormTotals con rateTotals[rate.id] = { ventasBase, ventasIva, comprasBase, comprasIva }:

base = crcIva / rate.percent            // percent>0; conversión a CRC si la factura es en otra moneda

Ventas además calcula detalleTotals (categorías exentas/exoneradas/no sujetas, con/sin crédito, bienes de capital), pagoDiferidoTotals (IVA contado vs. crédito para saleCondition === "10") y el sistema especial (TE). formFieldMaps.BASE_RULES arma los campos por prefijo (vg_, cp_, ct_, cf_, vg_pd_) y emite warnings si el IVA por modalidad no cuadra con el total de ventas.

D-153 (simplificado, solo 1/2/13 %) — D153Helpers.ts

buildD153DataFromRows(rows, fromDate, toDate, activityLabels, factorOverrides?):

  • RATE_CONFIG local solo 1/2/13 %.
  • Por actividad económica simplificada: un acordeón padre y, por tarifa, sub-campos compras_del_periodo, factor_{rate}, monto_del_impuesto.
  • Acumulación sobre filas de gasto:
    • Actividad ocupa (minisúper/bebidas): todo totalAmount (CRC) va al bucket 13 %.
    • Normal: por tarifa, base += (ivaNeto / rate.percent) + ivaNeto (base imponible + el propio IVA).
  • taxValue = base × factor (factor por actividad, override posible). Resumen: d153_impuesto_determinado = Σ taxValue.

D-104 / D-105 simplificado (ISU) — D104Helpers.ts, D105Helpers.ts

Por actividad: base = Σ (totalAmount − totalOtrosCargos) (CRC); monto_impuesto = base × factorISU (override posible). Resumen d10x_impuesto_determinado.

Taxi: buildD104TaxiData(totalRecorrido, …)monto = totalRecorrido × factorISU; mergeD104WithTaxiSection inserta la sección taxi (CIIU 4922.2, tipo “ocupa”) y suma al total.

Prorrata — useLibroProrrata.ts

14 categorías (MANUAL_PRORRATA_FIELDS). Fuente "ledger": desde las ventas del año anterior (taxable = Σ taxableSubtotal, exempt = Σ exemptSubtotal, rate = taxable/total). Fuente "manual": suma de campos. Persistencia en oldIngresos/{previousYear}.

Redondeo y precisión

  • Todo en number (float). Tolerancias: 2 CRC (gravado vs. TotalVentaNeta), 0.01 (limpieza de buckets en override).
  • Divisiones siempre con guarda rate.percent > 0.
  • Formato de moneda en lib/utils/formatting.ts.