endpoint.pe

Cómo calcular el IGV en tu sistema sin errores de redondeo

Calcular IGV al 18 % en tu sistema: precio con y sin IGV, redondeo correcto, totales por línea, operaciones exoneradas e inafectas y código en JS y Python.

eEquipo endpoint.pe•15 jun. 20269 min de lectura

El IGV parece el cálculo más fácil del sistema: multiplicas por 0.18 y listo. Hasta que una factura de cuarenta líneas suma S/ 0.01 más en el total que en el XML, SUNAT la observa y el contador te pide que "cuadre". O hasta que el precio de lista está con IGV incluido, el cliente quiere ver la base y tus decimales no cierran.

El problema nunca es la tasa. Es el redondeo, el orden de las operaciones y el tipo de dato con el que trabajas. Un float que da 0.30000000000000004 al sumar 0.1 y 0.2 no es un bug exótico: es exactamente lo que pasa cuando calculas impuestos con números binarios.

Aquí vas a ver las fórmulas, cómo redondear sin que los totales se desarmen, cuándo calcular por línea y cuándo por documento, qué operaciones no llevan IGV y código en JavaScript y Python listo para adaptar a tu facturador o ERP.


La tasa y las dos fórmulas

El IGV en el Perú es 18 %. En realidad son dos impuestos que se cobran juntos: 16 % de IGV propiamente dicho y 2 % de Impuesto de Promoción Municipal (IPM). Para tu sistema es un solo porcentaje.

Hay dos direcciones de cálculo y las dos tienen que existir en tu código.

De la base al precio final. Tienes el valor de venta (sin IGV) y quieres el precio que paga el cliente:

  • IGV = base × 0.18
  • Precio con IGV = base × 1.18

Si la base es S/ 100.00, el IGV es S/ 18.00 y el precio final S/ 118.00.

Del precio final a la base. Tienes el precio con IGV incluido (típico en retail, listas de precios al público, e-commerce) y necesitas desagregar:

  • Base = precio con IGV / 1.18
  • IGV = precio con IGV menos base

Si el precio al público es S/ 118.00, la base es S/ 100.00. El error clásico es calcular el IGV como precio × 0.18 en vez de dividir: con S/ 118.00 te daría S/ 21.24 de "IGV", que es incorrecto.

No escribas 0.18 en veinte lugares del código. Guarda la tasa en una constante o, mejor, en una tabla con vigencia por fecha: la tasa general ha cambiado antes (fue 19 % hasta 2011) y han existido tasas reducidas temporales para sectores específicos.

En el comprobante electrónico esta diferencia tiene nombre: valor unitario (sin IGV) y precio de venta unitario (con IGV). Guarda uno y deriva el otro; nunca pidas los dos al usuario.

El problema del redondeo

Los montos en el comprobante van con dos decimales. Pero S/ 33.33 × 0.18 = 5.9994, y S/ 7.99 × 3 × 0.18 = 4.3146. Hay que redondear, y la pregunta es cuándo y cómo.

Usa redondeo "half up" (0.005 sube a 0.01), que es el que espera cualquier contador. Cuidado con dos trampas:

  • Math.round en JavaScript redondea bien en teoría, pero 1.005 * 100 da 100.49999 por la representación binaria, así que Math.round(1.005 * 100) / 100 devuelve 1.00 y no 1.01.
  • round() de Python usa redondeo bancario: round(2.675, 2) puede dar 2.67, y round(0.5) da 0.

La solución robusta es no usar decimales flotantes para dinero. Dos opciones:

  1. Trabajar en céntimos con enteros. S/ 118.00 se guarda como 11800. Multiplicas, divides y redondeas enteros. Es lo que hacen la mayoría de pasarelas de pago.
  2. Usar un tipo decimal. Decimal en Python, BigDecimal en Java, decimal en C#, una columna DECIMAL(12,2) en tu base de datos. En JavaScript no hay tipo nativo; usa una librería de decimales o céntimos enteros.

No guardes precios en columnas FLOAT o DOUBLE: usa DECIMAL(12,2) para montos y DECIMAL(12,4) o más para precios unitarios y tipos de cambio.

Por línea o por documento

Esta es la decisión que más facturas observadas genera. Tienes dos formas de llegar al total:

Por documento. Sumas todas las bases, calculas el IGV sobre esa suma y redondeas una vez. Es simple y da el resultado "matemáticamente correcto" del total.

Por línea. Calculas base e IGV de cada línea, redondeas cada una a dos decimales y sumas. El IGV total es la suma de los IGV por línea.

Los dos métodos pueden diferir en uno o dos céntimos en facturas con muchas líneas. Como el comprobante electrónico lleva montos por línea y totales, y SUNAT valida que sean consistentes, calcular el total por documento y las líneas por separado produce observaciones.

La práctica más segura es calcular por línea, redondear por línea y sumar, de modo que el total sea literalmente la suma de las líneas. Así el XML es consistente por construcción. SUNAT aplica una tolerancia pequeña en algunas validaciones de totales, pero no diseñes tu cálculo contando con ella.

Un ejemplo con tres líneas gravadas:

LíneaCantidadValor unitarioBaseIGV de la línea
A37.9923.974.31
B133.3333.336.00
C71.158.051.45
Total65.3511.76

Por documento, 65.35 × 0.18 = 11.763, redondeado 11.76. Aquí coincide; con más líneas y decimales habrá facturas donde no, y ahí quieres un solo criterio.

Descuentos y cargos

Un descuento por línea reduce la base de esa línea antes de calcular el IGV. Un descuento global sobre toda la factura se prorratea o se aplica sobre la base gravada, pero igual debe reflejarse antes del impuesto. Nunca apliques descuentos sobre el precio con IGV y luego "retrocedas" el impuesto: acumulas diferencias de céntimos.

Facturas en dólares

Si la factura es en dólares, el IGV se calcula en dólares con la misma lógica; la conversión a soles, para tus libros, usa el tipo de cambio de la fecha de emisión. La API de tipo de cambio de endpoint.pe devuelve el vigente con compra y venta (guárdalo a diario para tener el de cada fecha de emisión), y en operaciones en dólares: compra o venta está explicado cuál usar.

Gravado, exonerado e inafecto

No todo lo que vendes lleva IGV, y tu sistema tiene que saberlo a nivel de ítem, no de factura. Una misma factura puede tener líneas gravadas y líneas exoneradas. A grandes rasgos:

  • Gravado. La operación está dentro del IGV y se aplica el 18 %. Es el caso por defecto para bienes y servicios en el país.
  • Exonerado. La operación está dentro del ámbito del impuesto, pero la ley la libera. El ejemplo típico son productos agrícolas básicos que figuran en los apéndices de la ley del IGV. La línea va con IGV cero y se marca como exonerada.
  • Inafecto. La operación está fuera del ámbito del impuesto. No es que se le perdone: simplemente no le aplica.
  • Exportación. La venta al exterior de bienes o servicios tiene tratamiento propio, sin IGV trasladado.
  • Gratuito. Si entregas algo sin cobrar (muestra, bonificación), la línea se marca como operación gratuita con su valor referencial, y el tratamiento del IGV es distinto al de una venta normal.

Cada línea del comprobante lleva un código de afectación del catálogo de SUNAT; cópialo a una tabla y asócialo a cada producto. Para el cálculo: gravado, exonerado e inafecto se suman por separado, y el IGV se calcula solo sobre el total gravado.

Si tu catálogo de productos no tiene un campo de afectación, agrégalo ahora. Un producto exonerado facturado como gravado cobra de más al cliente; uno gravado facturado como exonerado es un impuesto no declarado. Qué producto va en cada categoría lo define la norma, no el sistema: consulta la fuente oficial o al contador.

Otros conceptos que pueden aparecer en una factura no son IGV: el ISC, el impuesto a las bolsas plásticas, las detracciones, percepciones y retenciones. Cada uno tiene su propio lugar en el XML; no los mezcles en el cálculo. Si estás armando el emisor desde cero, la guía de facturación electrónica para desarrolladores explica dónde entra cada campo.

Código en JavaScript y Python

Las dos implementaciones calculan por línea, redondean half up y suman. JavaScript trabaja en céntimos enteros; Python usa Decimal. En producción, convierte las entradas desde el string original, no desde un float intermedio.

const TASA_IGV = 18; // por ciento; cárgala desde configuración

// Redondeo half up de un valor en céntimos que puede traer fracción
function redondearCentimos(valor) {
  return Math.floor(valor + 0.5);
}

// cantidad y valorUnitarioSinIgv en unidades normales (soles)
function calcularLinea(cantidad, valorUnitarioSinIgv, afectacion = 'gravado') {
  const base = redondearCentimos(cantidad * valorUnitarioSinIgv * 100);
  const igv = afectacion === 'gravado'
    ? redondearCentimos((base * TASA_IGV) / 100)
    : 0;
  return { base, igv, total: base + igv, afectacion }; // todo en céntimos
}

// Del precio con IGV incluido (en céntimos) a la base
function desagregar(precioConIgv) {
  const base = redondearCentimos((precioConIgv * 100) / (100 + TASA_IGV));
  return { base, igv: precioConIgv - base };
}

function totalesDocumento(lineas) {
  const acc = { gravado: 0, exonerado: 0, inafecto: 0, igv: 0 };
  for (const l of lineas) {
    acc[l.afectacion] += l.base;
    acc.igv += l.igv;
  }
  acc.total = acc.gravado + acc.exonerado + acc.inafecto + acc.igv;
  return acc;
}

const lineas = [
  calcularLinea(3, 7.99),
  calcularLinea(1, 33.33),
  calcularLinea(7, 1.15),
  calcularLinea(2, 4.5, 'exonerado'),
];
console.log(totalesDocumento(lineas));
// { gravado: 6535, exonerado: 900, inafecto: 0, igv: 1176, total: 8611 }
console.log(desagregar(11800)); // { base: 10000, igv: 1800 }
from decimal import Decimal, ROUND_HALF_UP

TASA_IGV = Decimal("18")  # por ciento; cárgala desde configuración
CENTIMO = Decimal("0.01")


def redondear(valor: Decimal) -> Decimal:
    return valor.quantize(CENTIMO, rounding=ROUND_HALF_UP)


def calcular_linea(cantidad, valor_unitario_sin_igv, afectacion="gravado"):
    cantidad = Decimal(str(cantidad))
    valor_unitario = Decimal(str(valor_unitario_sin_igv))
    base = redondear(cantidad * valor_unitario)
    if afectacion == "gravado":
        igv = redondear(base * TASA_IGV / 100)
    else:
        igv = Decimal("0.00")
    return {"base": base, "igv": igv, "total": base + igv, "afectacion": afectacion}


def desagregar(precio_con_igv):
    precio = Decimal(str(precio_con_igv))
    base = redondear(precio * 100 / (100 + TASA_IGV))
    return {"base": base, "igv": precio - base}


def totales_documento(lineas):
    acc = {k: Decimal("0.00") for k in ("gravado", "exonerado", "inafecto", "igv")}
    for linea in lineas:
        acc[linea["afectacion"]] += linea["base"]
        acc["igv"] += linea["igv"]
    acc["total"] = acc["gravado"] + acc["exonerado"] + acc["inafecto"] + acc["igv"]
    return acc


lineas = [
    calcular_linea(3, "7.99"),
    calcular_linea(1, "33.33"),
    calcular_linea(7, "1.15"),
    calcular_linea(2, "4.50", "exonerado"),
]
print(totales_documento(lineas))
# gravado 65.35, exonerado 9.00, inafecto 0.00, igv 11.76, total 86.11
print(desagregar("118.00"))  # base 100.00, igv 18.00

Escribe pruebas unitarias antes de conectar esto al generador de XML: 100 debe dar 18; 118 con IGV incluido, base 100; 33.33, IGV 6.00; una línea exonerada, IGV 0 sin tocar el total gravado. Cuatro pruebas que te ahorran una tarde de facturas observadas.

En resumen

  • IGV = 18 % (16 % IGV + 2 % IPM). Base × 1.18 da el precio final; precio final / 1.18 da la base. Nunca precio final × 0.18.
  • Guarda la tasa como configuración con vigencia, no como literal en el código.
  • No uses float para dinero: céntimos enteros o un tipo decimal, y columnas DECIMAL en la base de datos.
  • Calcula y redondea por línea (half up), y que el total sea la suma de las líneas.
  • Cada producto necesita su afectación (gravado, exonerado, inafecto, exportación, gratuito). El IGV se calcula solo sobre el total gravado.
  • Los descuentos van antes del impuesto; las facturas en dólares calculan el IGV en dólares y convierten con el tipo de cambio de la fecha.

Preguntas frecuentes

¿El IGV se calcula antes o después del descuento?

Después del descuento, es decir, sobre la base ya rebajada. Si un producto de S/ 100.00 tiene 10 % de descuento, la base es S/ 90.00 y el IGV S/ 16.20. El descuento reduce el valor de venta; el impuesto se aplica sobre lo que realmente se cobra.

¿Qué hago si la base calculada desde el precio con IGV no da exacta?

Es normal: S/ 10.00 con IGV incluido da una base de 8.4745..., que redondeas a 8.47 y el IGV queda como diferencia, 1.53. Siempre calcula una de las dos cifras y obtén la otra por resta, para que base más IGV sea exactamente el precio final.

¿En una boleta también se desagrega el IGV?

El comprobante electrónico lleva base e IGV desagregados aunque la representación impresa de la boleta muestre precios con IGV incluido. Si dudas entre emitir factura o boleta, revisa factura o boleta: cuándo emitir cada una.

¿Un negocio del NRUS calcula IGV?

No en sus boletas: el Nuevo RUS paga una cuota única que reemplaza al IGV y al impuesto a la renta, y no puede emitir facturas. Si tu sistema atiende a emisores de distintos regímenes, el cálculo de IGV debe poder desactivarse según el régimen del emisor.

Deja de copiar datos a mano. Automatízalo hoy mismo

Crea tu cuenta, copia tu token y empieza hoy con 150 consultas gratis al mes.