Ir al contenido

Factura Electrónica

La Factura Electrónica (código SUNAT 01) y la Boleta Electrónica (código SUNAT 03) son los comprobantes de venta más usados en el sistema de emisión electrónica de SUNAT. openUBL genera el XML UBL 2.1 listo para firmar digitalmente y enviar a SUNAT.

  • Factura (01): se emite ante operaciones con clientes que tienen RUC y que necesitan crédito fiscal.
  • Boleta (03): se emite ante clientes finales que no necesitan crédito fiscal.

En openUBL ambos documentos comparten el mismo modelo (Invoice) y se diferencian únicamente por la serie: las facturas usan series que empiezan con F (F001), y las boletas con B (B001).

CampoTipoDescripción y validación
seriestringSerie del comprobante. Debe iniciar con F para factura o B para boleta, seguido de 2 o 3 caracteres alfanuméricos. Ejemplos: F001, B001.
numerointegerCorrelativo del comprobante. Ejemplo: 1, 123.
proveedorProveedorDatos del emisor. Requiere ruc (11 dígitos numéricos) y razonSocial.
clienteClienteDatos del adquirente. Requiere nombre, numeroDocumentoIdentidad y tipoDocumentoIdentidad (Catálogo N.° 06).
detallesDocumentoVentaDetalle[]Lista de líneas de producto o servicio. Debe contener al menos un ítem.

Cada elemento de detalles requiere:

CampoTipoDescripción
descripcionstringNombre del producto o servicio.
cantidadDecimalCantidad vendida. Debe ser mayor que 0.
precioDecimalPrecio unitario sin IGV.
CampoDefaultDescripción
monedaPENCódigo de moneda según Catálogo N.° 02 (PEN, USD, EUR).
tipoOperacion0101Tipo de operación según Catálogo N.° 51. 0101 = Venta interna.
fechaEmisionhoyFecha de emisión en formato YYYY-MM-DD. Si no se envía, se asigna la fecha actual.
igvTotalautoSuma del IGV de todas las líneas. Calculado automáticamente por ContentEnricher.
valorVentaTotalautoSuma del valor de venta de todas las líneas. Calculado automáticamente.
importeTotalautoTotal a pagar (valorVentaTotal + igvTotal). Calculado automáticamente.
CampoDefaultCatálogoDescripción
unidadMedidaNIUN.° 03Unidad de medida. NIU = Unidad.
tipoAfectacionIGV10N.° 07Tipo de afectación del IGV.
igvautoIGV de la línea. Se calcula según el tipo de afectación.
valorVentaautoValor de venta de la línea (cantidad × precio).
precioVentaautoPrecio de venta de la línea (valorVenta + igv).

openUBL aplica, además del esquema UBL 2.1, las siguientes reglas de validación documentadas por SUNAT:

CódigoRegla
2074UBLVersionID debe ser 2.1.
2072CustomizationID debe ser 2.0.
1001El ID del comprobante debe tener el formato {serie}-{numero}.
2070DocumentCurrencyCode es obligatorio.
1007El schemeID del emisor debe ser 6 (RUC).
1008El RUC del emisor debe tener 11 dígitos.
1037La razón social del emisor (RegistrationName) es obligatoria.
2015El schemeID del cliente es obligatorio.
2062El importe total a pagar (PayableAmount) debe ser mayor a 0.
3305TaxInclusiveAmount es obligatorio.
3294El TaxTotal debe cuadrar con la sumatoria de los impuestos de las líneas.
3278El LineExtensionAmount debe cuadrar con la sumatoria de los valores de venta de las líneas.

Estas reglas se ejecutan automáticamente cuando llamas a la API con ?validate=true (valor por defecto). También puedes ejecutarlas manualmente con SunatValidator.validate_invoice(xml).

Para emitir un comprobante con varios productos o servicios, incluye cada uno como un elemento del array detalles.

from decimal import Decimal
from openubl.models import Invoice, Proveedor, Cliente, DocumentoVentaDetalle
invoice = Invoice(
serie="F001",
numero=5,
proveedor=Proveedor(ruc="20100100100", razonSocial="Mi Empresa S.A.C."),
cliente=Cliente(
nombre="Juan Pérez",
numeroDocumentoIdentidad="46779327",
tipoDocumentoIdentidad="1", # DNI
),
detalles=[
DocumentoVentaDetalle(
descripcion="Laptop Dell XPS 13",
cantidad=Decimal("1"),
precio=Decimal("4800.00"), # precio ya con descuento reflejado
),
DocumentoVentaDetalle(
descripcion="Mouse inalámbrico",
cantidad=Decimal("2"),
precio=Decimal("150.00"),
),
],
)

El campo tipoOperacion indica la naturaleza de la operación documentada. openUBL usa 0101 (Venta interna) por defecto. Catálogo N.° 51 de SUNAT incluye muchos más códigos; los más comunes son:

CódigoDescripción
0101Venta interna
0200Exportación de bienes
0401Venta no domiciliado que no califica como exportación

Para operaciones de exportación usa 0200 y asegúrate de que las líneas usen el tipo de afectación IGV correspondiente.

Cada línea define su tipo de afectación mediante tipoAfectacionIGV. openUBL soporta los valores más comunes del Catálogo N.° 07:

CódigoDescripciónComportamiento del enriquecimiento
10Gravado — Operación onerosaCalcula IGV (valorVenta × 18%) y lo suma al precio de venta.
20Exonerado — Operación onerosaIGV = 0. El precio de venta es igual al valor de venta.
30Inafecto — Operación onerosaIGV = 0. El precio de venta es igual al valor de venta.

Ejemplo de una línea exonerada:

DocumentoVentaDetalle(
descripcion="Medicamento exonerado",
cantidad=Decimal("2"),
precio=Decimal("50.00"),
tipoAfectacionIGV="20",
)

El campo moneda acepta códigos del Catálogo N.° 02:

CódigoDescripción
PENSoles
USDDólares estadounidenses
EUREuros

Cuando emitas en moneda extranjera, establece moneda en USD o EUR. El enriquecimiento calcula los totales en esa moneda; SUNAT recibirá el XML con DocumentCurrencyCode correspondiente.

Invoice(
serie="F001",
numero=10,
moneda="USD",
# ... proveedor, cliente y detalles
)

Este ejemplo crea una factura, la enriquece, renderiza el XML UBL 2.1 y la valida contra las reglas SUNAT:

from decimal import Decimal
from openubl.models import Invoice, Proveedor, Cliente, DocumentoVentaDetalle
from openubl.enricher import ContentEnricher
from openubl.renderer import render_invoice
from openubl.validator import SunatValidator
invoice = Invoice(
serie="F001",
numero=1,
proveedor=Proveedor(
ruc="20100100100",
razonSocial="Mi Empresa S.A.C.",
),
cliente=Cliente(
nombre="Juan Pérez",
numeroDocumentoIdentidad="46779327",
tipoDocumentoIdentidad="1", # DNI
),
detalles=[
DocumentoVentaDetalle(
descripcion="Laptop Dell XPS 13",
cantidad=Decimal("1"),
precio=Decimal("5000.00"),
),
],
)
# 1. Enriquecer: calcula fechas, IGV y totales
enricher = ContentEnricher()
enricher.enrich(invoice)
# 2. Renderizar a XML UBL 2.1
xml = render_invoice(invoice)
# 3. Validar contra reglas SUNAT
validator = SunatValidator()
errors = validator.validate_invoice(xml)
if errors:
print("Errores de validación:", errors)
else:
print("XML generado correctamente")
print(xml[:500])

Si usas la API REST, el enriquecimiento se ejecuta automáticamente. Desde Python también puedes personalizar las tasas o la fecha:

from openubl.enricher import ContentEnricher
from openubl.models.defaults import Defaults, DateProvider
from datetime import date
# Forzar IGV al 18% y simular una fecha específica para tests
enricher = ContentEnricher(
defaults=Defaults(igvTasa=Decimal("0.18"), icbTasa=Decimal("0.2")),
date_provider=DateProvider(), # por defecto devuelve date.today()
)
# Para testing puedes crear tu propio proveedor de fechas
class FixedDateProvider(DateProvider):
@staticmethod
def now():
return date(2026, 6, 11)
enricher = ContentEnricher(date_provider=FixedDateProvider())
enricher.enrich(invoice)

Genera una factura o boleta llamando al endpoint de creación:

import httpx
from openubl.models import Invoice, Proveedor, Cliente, DocumentoVentaDetalle
invoice = Invoice(
serie="F001",
numero=1,
proveedor=Proveedor(ruc="20100100100", razonSocial="Mi Empresa S.A.C."),
cliente=Cliente(
nombre="Juan Pérez",
numeroDocumentoIdentidad="46779327",
tipoDocumentoIdentidad="1",
),
detalles=[
DocumentoVentaDetalle(
descripcion="Laptop Dell XPS 13",
cantidad=1,
precio=5000.00,
),
],
)
with httpx.Client(base_url="http://localhost:8000") as client:
response = client.post("/api/v1/invoice/create?validate=true", json=invoice.model_dump(mode="json"))
print(response.json())

Para omitir la validación SUNAT (por ejemplo, mientras depuras el JSON), usa ?validate=false:

import httpx
from openubl.models import Invoice, Proveedor, Cliente, DocumentoVentaDetalle
invoice = Invoice(
serie="F001",
numero=1,
proveedor=Proveedor(ruc="20100100100", razonSocial="Mi Empresa S.A.C."),
cliente=Cliente(
nombre="Juan Pérez",
numeroDocumentoIdentidad="46779327",
tipoDocumentoIdentidad="1",
),
detalles=[
DocumentoVentaDetalle(
descripcion="Laptop Dell XPS 13",
cantidad=1,
precio=5000.00,
),
],
)
with httpx.Client(base_url="http://localhost:8000") as client:
response = client.post("/api/v1/invoice/create?validate=false", json=invoice.model_dump(mode="json"))
print(response.json())

Cuando la solicitud es exitosa, la API responde con estado 200 OK y un objeto JSON que contiene el XML generado:

{
"xml": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>..."
}

Si la validación SUNAT detecta errores, la API responde con estado 422 Unprocessable Entity y un array con los mensajes de error:

{
"detail": [
"ERROR 1008: RUC del emisor debe tener 11 dígitos",
"ERROR 2062: PayableAmount debe ser mayor a 0"
]
}

El campo xml contiene el documento UBL 2.1 completo, listo para firmar digitalmente con el endpoint POST /api/v1/sign o con la librería de firmado de openUBL. `,