djasoft / sunat-comprobantes
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.
Requires
- php: ^8.2
- luecano/numero-a-letras: ^4.0
Requires (Dev)
- phpunit/phpunit: ^11.0 || ^12.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
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:
- 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.
- 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, nuncaround(). La diferencia se reporta enredondeo. - 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ílchez — Djasoft · @DanielMoranV