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_5 | 0.005 | IVA(0.5%) |
rate_1 | 0.01 | IVA(1%) |
rate_2 | 0.02 | IVA(2%) |
rate_3 | 0.03 | IVA(3%) |
rate_4 | 0.04 | IVA(4%) |
rate_13 | 0.13 | IVA(13%) |
Mapeos de códigos de Hacienda en lib/constants/invoiceCodes.ts:
CODIGO_TARIFA_IVA— código → etiqueta (08→IVA(13%),02→IVA(1%),09→IVA(0.5%),10→IVA(Exenta),11→IVA(0% sin credito), …).CODIGO_NUMERO_TARIFA_IVA— código → tarifa numérica (para detectar mismatch).CODIGO_IMPUESTO_SIGLAS—01→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
CodigoTarifaIVAes válido (≠99): compara la tarifa esperada (CODIGO_NUMERO_TARIFA_IVA[code]) con la tarifa real (Impuesto.Tarifa):- Coinciden → acumula en
ivaPorTarifa[key]yivaPorTarifaNeto[key]. - No coinciden + GASTO → usa la tarifa real para el bucket, marca
tarifaMismatch = truey guardatarifaMismatchDetails{ resolvedKey, originalCode, actualTarifa, monto, montoNeto, lineaBase }. - No coinciden + VENTA (o sin bucket) → va a TE (impuesto de tarifa especial):
teIva += monto,teBase += linea.MontoTotal.
- Coinciden → acumula en
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
TotalVentaNetadentro 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 comomismatchOverride∈"tarifa" | "codigo" | "te". - Aplicación:
LibrosTab.applyMismatchOverridesToRows()(memoizada endisplayRows):"tarifa"→ no toca el desglose (ya está en la tarifa real); solo recalcula taxable/exempt."codigo"→ restamonto/montoNetodel bucket real y los suma al bucket del código original (CODIGO_TARIFA_IVA[originalCode])."te"→ resta del bucket real y suma ate+totalTe.- Limpia buckets con
|valor| < 0.01, recalculavatAmounty taxable/exempt.
- UI:
TarifaMismatchModal(select por fila). Al recargar un libro guardado, el override se rehidrata desderow.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égimen | Formulario | Periodicidad |
|---|---|---|
| Tradicional | D-150 | Mensual |
| Simplificado | D-153 (si hay factor IVA), D-104 (físico) o D-105 (jurídico) | Trimestral |
| Especial agropecuario | D-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 monedaVentas 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_CONFIGlocal 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): todototalAmount(CRC) va al bucket 13 %. - Normal: por tarifa,
base += (ivaNeto / rate.percent) + ivaNeto(base imponible + el propio IVA).
- Actividad
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.