Search by

djasoft / sunat-comprobantes

DanielMoranV

Cálculo de totales e IGV y contenido del código QR para comprobantes de pago electrónicos de SUNAT (Perú). Sin dependencias de framework.

Package info

github.com/DanielMoranV/sunat-comprobantes

pkg:composer/djasoft/sunat-comprobantes

Statistics

Installs: 1

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v1.0.0 2026-08-15 17:42 UTC

This package is auto-updated.

Last update: 2026-08-15 18:02:51 UTC


README

Dos piezas que todo el que factura electrónicamente en Perú acaba escribiendo: el cálculo de totales e IGV de un comprobante y el contenido del código QR de la representación impresa.

Sin dependencias de framework. Funciona igual en Laravel, Symfony, Slim o PHP a secas.

composer require djasoft/sunat-comprobantes

Por qué existe

La facturación electrónica peruana está bien resuelta en la parte difícil: Greenter genera el XML, lo firma y lo envía a SUNAT. Pero antes de llegar ahí hay dos cosas que cada proyecto reimplementa:

  1. Calcular los totales. Separar las bases imponibles por tipo de afectación, sumar el IGV y el ICBPER, aplicar el redondeo hacia abajo al décimo, y generar la leyenda del importe en letras. Son reglas de SUNAT, no de tu negocio.
  2. Armar el QR. Diez campos separados por |, en un orden exacto y con un | final que se olvida muy fácilmente.

Ninguna de las dos es complicada. Las dos son fáciles de equivocar, y el error se descubre tarde: cuando SUNAT rechaza el comprobante.

Este paquete sale de un ERP en producción y viene con 18 tests.

Cálculo de totales

Calculator trabaja con arrays planos y usa los mismos nombres de campo que Greenter, así que el resultado se le pasa directamente.

use Djasoft\Sunat\Calculator;

$calc = new Calculator;

// buildDetail recibe el precio unitario CON IGV, que es el que ve el cliente
// y el que sueles tener guardado. Él calcula el valor sin IGV.
$data = ['details' => [
    $calc->buildDetail('SKU-001', 'NIU', 'Teclado mecánico', 2, 118.00),
    $calc->buildDetail('SKU-002', 'NIU', 'Libro', 1, 50.00, Calculator::EXONERADO),
]];

$calc->calculate($data);

$data['mtoOperGravadas'];   // 200.00
$data['mtoOperExoneradas']; //  50.00
$data['mtoIGV'];            //  36.00
$data['valorVenta'];        // 250.00
$data['subTotal'];          // 286.00
$data['mtoImpVenta'];       // 286.00  (redondeado al décimo inferior)
$data['redondeo'];          //   0.00
$data['legends'][0]['value']; // "DOSCIENTOS OCHENTA Y SEIS CON 00/100 SOLES"

Tipos de afectación del catálogo 07, como constantes:

Constante Código Significado
Calculator::GRAVADO 10 Gravado con IGV
Calculator::EXONERADO 20 Exonerado
Calculator::INAFECTO 30 Inafecto
Calculator::EXPORTACION 40 Exportación

Cualquier otro código se trata como operación gratuita: su base va a mtoOperGratuitas y su IGV a mtoIGVGratuitas, sin sumar al importe a pagar.

Dos detalles que suelen costar un rechazo

  • El redondeo va hacia abajo. mtoImpVenta = floor(subTotal * 10) / 10, nunca round(). La diferencia se reporta en redondeo.
  • El valor unitario lleva 6 decimales, no 2. SUNAT valida el detalle con esa precisión y redondear antes de tiempo descuadra el comprobante.

Si tu IGV no es 18%, pásalo como último argumento de buildDetail. Y si facturas en dólares, new Calculator('DOLARES AMERICANOS') cambia la moneda de la leyenda.

Contenido del código QR

use Djasoft\Sunat\QrContent;

$contenido = (new QrContent)->build(
    ruc:            '20123456789',
    tipoDocumento:  QrContent::FACTURA,
    serie:          'F001',
    correlativo:    '00000123',
    igv:            18.00,
    total:          118.00,
    fechaEmision:   '2026-08-15',
    tipoDocCliente: '6',
    numDocCliente:  '20987654321',
    hash:           $digestDelXmlFirmado,
);

// 20123456789|01|F001|00000123|18.00|118.00|2026-08-15|6|20987654321|abc…|

En una boleta a consumidor final omite los datos del cliente: por defecto quedan como 0 y -, que es lo que espera SUNAT.

Esta clase solo produce la cadena. Renderizarla es cosa tuya y de la librería que prefieras:

use BaconQrCode\Renderer\{ImageRenderer, RendererStyle\RendererStyle};
use BaconQrCode\Renderer\Image\SvgImageBackEnd;
use BaconQrCode\Writer;

$svg = (new Writer(new ImageRenderer(new RendererStyle(220, 1), new SvgImageBackEnd)))
    ->writeString($contenido, 'UTF-8');

Alcance

Esto no envía nada a SUNAT ni genera XML: para eso está Greenter, y no tiene sentido reescribirlo. Aquí solo están las dos piezas de cálculo y formato que Greenter no cubre.

Requisitos

PHP 8.2+ y luecano/numero-a-letras para la leyenda del importe.

Tests

composer install
composer test

Licencia

MIT. Úsalo en lo que quieras, incluido software comercial.

Autor

Daniel Morán VílchezDjasoft · @DanielMoranV