Enriquecimiento automático
El enriquecimiento automático es el paso que completa los campos calculables de un comprobante antes de renderizarlo a XML. En lugar de escribir manualmente totales, impuestos y fechas, ContentEnricher los deriva de los datos mínimos que ya proporcionaste.
Este patrón respeta la normativa peruana:
- Ley N.° 30296 — IGV tasa 18 %.
- Ley N.° 30830 — ICBPER tasa S/ 0.20.
- RS N.° 300-2014/SUNAT, Anexo 1 — estructura del comprobante electrónico.
¿Qué hace ContentEnricher?
Sección titulada «¿Qué hace ContentEnricher?»ContentEnricher recibe un documento y lo modifica en memoria (in-place) agregando o completando los campos que faltan. Soporta:
Invoice(Factura / Boleta)CreditNote(Nota de Crédito)DebitNote(Nota de Débito)VoidedDocuments(Comunicación de Baja)
Resumen Diario, Percepción y Retención no requieren enriquecimiento porque sus totales se definen explícitamente.
Campos calculados automáticamente
Sección titulada «Campos calculados automáticamente»| Campo | Nivel | Fórmula / Regla |
|---|---|---|
fechaEmision | Documento | Fecha actual si no se indica (date.today()). |
valorVenta | Línea | cantidad × precio, redondeado a 2 decimales. |
igv | Línea | valorVenta × igvTasa solo si tipoAfectacionIGV == "10" (Gravado); de lo contrario 0. |
precioVenta | Línea | valorVenta + igv, redondeado a 2 decimales. |
valorVentaTotal | Documento | Suma de valorVenta de todas las líneas. |
igvTotal | Documento | Suma de igv de todas las líneas. |
importeTotal | Documento | valorVentaTotal + igvTotal. |
Los redondeos usan ROUND_HALF_UP a 2 decimales, el mismo criterio que aplica SUNAT para los comprobantes electrónicos.
Defaults
Sección titulada «Defaults»La clase Defaults centraliza las tasas usadas durante el enriquecimiento:
| Campo | Valor por defecto | Origen legal |
|---|---|---|
igvTasa | 0.18 | Ley N.° 30296 |
icbTasa | 0.20 | Ley N.° 30830 |
Puedes instanciar ContentEnricher sin argumentos y usará estos valores:
from openubl.enricher import ContentEnricher
enricher = ContentEnricher()// El enriquecimiento ocurre automáticamente al llamar a POST /api/v1/invoice/create.import { createInvoice } from "@openubl/sdk";import { zInvoice } from "@openubl/sdk/zod.gen";
const invoice = zInvoice.parse({serie: "F001",numero: 1,tipoOperacion: "0101",moneda: "PEN",proveedor: { ruc: "20100100100", razonSocial: "Mi Empresa S.A.C." },cliente: { tipoDocumentoIdentidad: "6", numeroDocumentoIdentidad: "12345678", nombre: "Cliente Ejemplo" },detalles: [{ descripcion: "Producto A", cantidad: 1, precio: 100, unidadMedida: "NIU", tipoAfectacionIGV: "10" }],});
const { data, error } = await createInvoice({body: invoice,});
if (error) throw new Error(JSON.stringify(error));# El enriquecimiento ocurre automáticamente al llamar a POST /api/v1/invoice/create.curl -X POST http://localhost:8000/api/v1/invoice/create -H "Content-Type: application/json" -d '{ "serie": "F001", "numero": 1, "tipoOperacion": "0101", "moneda": "PEN", "proveedor": { "ruc": "20100100100", "razonSocial": "Mi Empresa S.A.C." }, "cliente": { "tipoDocumentoIdentidad": "6", "numeroDocumentoIdentidad": "12345678", "nombre": "Cliente Ejemplo" }, "detalles": [{ "descripcion": "Producto A", "cantidad": 1, "precio": 100, "unidadMedida": "NIU", "tipoAfectacionIGV": "10" }]}'Ejemplo con valores custom
Sección titulada «Ejemplo con valores custom»Si necesitas una tasa diferente (por ejemplo, para un escenario histórico o de prueba), crea un Defaults personalizado:
from decimal import Decimalfrom openubl.enricher import ContentEnricherfrom openubl.models.defaults import Defaults
enricher = ContentEnricher( defaults=Defaults( igvTasa=Decimal("0.18"), icbTasa=Decimal("0.2"), ))// Las tasas de IGV/ICBPER se configuran en el servidor.// El endpoint aplica esos valores al enriquecer el documento.import { createInvoice } from "@openubl/sdk";import { zInvoice } from "@openubl/sdk/zod.gen";
const invoice = zInvoice.parse({serie: "F001",numero: 1,tipoOperacion: "0101",moneda: "PEN",proveedor: { ruc: "20100100100", razonSocial: "Mi Empresa S.A.C." },cliente: { tipoDocumentoIdentidad: "6", numeroDocumentoIdentidad: "12345678", nombre: "Cliente Ejemplo" },detalles: [{ descripcion: "Producto A", cantidad: 1, precio: 100, unidadMedida: "NIU", tipoAfectacionIGV: "10" }],});
const { data, error } = await createInvoice({body: invoice,});
if (error) throw new Error(JSON.stringify(error));# Las tasas de IGV/ICBPER se configuran en el servidor.# El endpoint aplica esos valores al enriquecer el documento.curl -X POST http://localhost:8000/api/v1/invoice/create -H "Content-Type: application/json" -d '{ "serie": "F001", "numero": 1, "tipoOperacion": "0101", "moneda": "PEN", "proveedor": { "ruc": "20100100100", "razonSocial": "Mi Empresa S.A.C." }, "cliente": { "tipoDocumentoIdentidad": "6", "numeroDocumentoIdentidad": "12345678", "nombre": "Cliente Ejemplo" }, "detalles": [{ "descripcion": "Producto A", "cantidad": 1, "precio": 100, "unidadMedida": "NIU", "tipoAfectacionIGV": "10" }]}'Aunque en el ejemplo se repiten los valores por defecto, este es el punto de entrada para cambiar la tasa de IGV o el monto del ICBPER cuando la normativa o tu negocio lo requieran.
DateProvider
Sección titulada «DateProvider»DateProvider es una clase que abstrae la obtención de la fecha actual. Su implementación por defecto devuelve date.today():
from openubl.models.defaults import DateProvider
provider = DateProvider()print(provider.now()) # 2026-06-11 (fecha actual)// El enriquecimiento automático en el servidor aplica la fecha actual cuando no se envía fechaEmision.import { createInvoice } from "@openubl/sdk";import { zInvoice } from "@openubl/sdk/zod.gen";
const invoice = zInvoice.parse({serie: "F001",numero: 1,tipoOperacion: "0101",moneda: "PEN",proveedor: { ruc: "20100100100", razonSocial: "Mi Empresa S.A.C." },cliente: { tipoDocumentoIdentidad: "6", numeroDocumentoIdentidad: "12345678", nombre: "Cliente Ejemplo" },detalles: [{ descripcion: "Producto A", cantidad: 1, precio: 100, unidadMedida: "NIU", tipoAfectacionIGV: "10" }],});
const { data, error } = await createInvoice({body: invoice,});
if (error) throw new Error(JSON.stringify(error));# El enriquecimiento automático en el servidor aplica la fecha actual cuando no se envía fechaEmision.curl -X POST http://localhost:8000/api/v1/invoice/create -H "Content-Type: application/json" -d '{ "serie": "F001", "numero": 1, "tipoOperacion": "0101", "moneda": "PEN", "proveedor": { "ruc": "20100100100", "razonSocial": "Mi Empresa S.A.C." }, "cliente": { "tipoDocumentoIdentidad": "6", "numeroDocumentoIdentidad": "12345678", "nombre": "Cliente Ejemplo" }, "detalles": [{ "descripcion": "Producto A", "cantidad": 1, "precio": 100, "unidadMedida": "NIU", "tipoAfectacionIGV": "10" }]}'Gracias a esta abstracción puedes reemplazar el proveedor de fechas en tus tests para obtener resultados deterministas.
Ejemplo con DateProvider para testing
Sección titulada «Ejemplo con DateProvider para testing»from datetime import datefrom openubl.enricher import ContentEnricherfrom openubl.models.defaults import DateProvider
class FixedDateProvider(DateProvider): @staticmethod def now(): return date(2025, 6, 1)
enricher = ContentEnricher(date_provider=FixedDateProvider())// El control de fechas deterministas se realiza con la librería Python (openubl.models.defaults.DateProvider).// En TypeScript envía explicitamente fechaEmision para obtener resultados reproducibles.import { createInvoice } from "@openubl/sdk";import { zInvoice } from "@openubl/sdk/zod.gen";
const invoice = zInvoice.parse({serie: "F001",numero: 1,tipoOperacion: "0101",moneda: "PEN",fechaEmision: "2025-06-01",proveedor: { ruc: "20100100100", razonSocial: "Mi Empresa S.A.C." },cliente: { tipoDocumentoIdentidad: "6", numeroDocumentoIdentidad: "12345678", nombre: "Cliente Ejemplo" },detalles: [{ descripcion: "Producto A", cantidad: 1, precio: 100, unidadMedida: "NIU", tipoAfectacionIGV: "10" }],});
const { data, error } = await createInvoice({body: invoice,});
if (error) throw new Error(JSON.stringify(error));# El control de fechas deterministas se realiza con la librería Python (openubl.models.defaults.DateProvider).# En cURL envía explicitamente fechaEmision para obtener resultados reproducibles.curl -X POST http://localhost:8000/api/v1/invoice/create -H "Content-Type: application/json" -d '{ "serie": "F001", "numero": 1, "tipoOperacion": "0101", "moneda": "PEN", "fechaEmision": "2025-06-01", "proveedor": { "ruc": "20100100100", "razonSocial": "Mi Empresa S.A.C." }, "cliente": { "tipoDocumentoIdentidad": "6", "numeroDocumentoIdentidad": "12345678", "nombre": "Cliente Ejemplo" }, "detalles": [{ "descripcion": "Producto A", "cantidad": 1, "precio": 100, "unidadMedida": "NIU", "tipoAfectacionIGV": "10" }]}'Con FixedDateProvider, cualquier documento que no especifique fechaEmision recibirá siempre 2025-06-01, haciendo que tus aserciones sean reproducibles.
Ejemplo completo
Sección titulada «Ejemplo completo»from decimal import Decimalfrom openubl.models import Invoice, Proveedor, Cliente, DocumentoVentaDetallefrom openubl.enricher import ContentEnricher
invoice = Invoice( serie="F001", numero=1, proveedor=Proveedor(ruc="20100066603", razonSocial="Mi Empresa S.A.C."), cliente=Cliente( nombre="Cliente Ejemplo", numeroDocumentoIdentidad="12345678", tipoDocumentoIdentidad="1", ), detalles=[ DocumentoVentaDetalle( descripcion="Producto A", cantidad=Decimal("10"), precio=Decimal("100"), ), DocumentoVentaDetalle( descripcion="Producto B", cantidad=Decimal("5"), precio=Decimal("200"), ), ],)
enricher = ContentEnricher()enricher.enrich(invoice)
assert invoice.valorVentaTotal == Decimal("2000.00")assert invoice.igvTotal == Decimal("360.00")assert invoice.importeTotal == Decimal("2360.00")// El enriquecimiento ocurre automáticamente al llamar a POST /api/v1/invoice/create.import { createInvoice } from "@openubl/sdk";import { zInvoice } from "@openubl/sdk/zod.gen";
const invoice = zInvoice.parse({serie: "F001",numero: 1,tipoOperacion: "0101",moneda: "PEN",proveedor: { ruc: "20100066603", razonSocial: "Mi Empresa S.A.C." },cliente: { tipoDocumentoIdentidad: "1", numeroDocumentoIdentidad: "12345678", nombre: "Cliente Ejemplo" },detalles: [ { descripcion: "Producto A", cantidad: 10, precio: 100, unidadMedida: "NIU", tipoAfectacionIGV: "10" }, { descripcion: "Producto B", cantidad: 5, precio: 200, unidadMedida: "NIU", tipoAfectacionIGV: "10" },],});
const { data, error } = await createInvoice({body: invoice,});
if (error) throw new Error(JSON.stringify(error));
console.log(data.valorVentaTotal); // 2000.00console.log(data.igvTotal); // 360.00console.log(data.importeTotal); // 2360.00# El enriquecimiento ocurre automáticamente al llamar a POST /api/v1/invoice/create.curl -X POST http://localhost:8000/api/v1/invoice/create -H "Content-Type: application/json" -d '{ "serie": "F001", "numero": 1, "tipoOperacion": "0101", "moneda": "PEN", "proveedor": { "ruc": "20100066603", "razonSocial": "Mi Empresa S.A.C." }, "cliente": { "tipoDocumentoIdentidad": "1", "numeroDocumentoIdentidad": "12345678", "nombre": "Cliente Ejemplo" }, "detalles": [ { "descripcion": "Producto A", "cantidad": 10, "precio": 100, "unidadMedida": "NIU", "tipoAfectacionIGV": "10" }, { "descripcion": "Producto B", "cantidad": 5, "precio": 200, "unidadMedida": "NIU", "tipoAfectacionIGV": "10" } ]}'Después del enriquecimiento, el documento tiene todos los totales calculados y está listo para renderizarse a XML, validarse, firmarse y enviarse a SUNAT.
Comportamiento por tipo de afectación IGV
Sección titulada «Comportamiento por tipo de afectación IGV»El enriquecidor respeta el Catálogo N.° 07. Solo los ítems gravados (10) generan IGV; exonerados (20) e inafectos (30) producen 0:
from decimal import Decimalfrom openubl.models import Invoice, Proveedor, Cliente, DocumentoVentaDetallefrom openubl.enricher import ContentEnricher
invoice = Invoice( serie="F001", numero=2, proveedor=Proveedor(ruc="20100066603", razonSocial="Mi Empresa S.A.C."), cliente=Cliente( nombre="Cliente Ejemplo", numeroDocumentoIdentidad="12345678", tipoDocumentoIdentidad="1", ), detalles=[ DocumentoVentaDetalle( descripcion="Gravado", cantidad=Decimal("1"), precio=Decimal("100"), tipoAfectacionIGV="10", ), DocumentoVentaDetalle( descripcion="Exonerado", cantidad=Decimal("1"), precio=Decimal("50"), tipoAfectacionIGV="20", ), ],)
enricher = ContentEnricher()enricher.enrich(invoice)
assert invoice.detalles[0].igv == Decimal("18.00")assert invoice.detalles[1].igv == Decimal("0.00")assert invoice.igvTotal == Decimal("18.00")// El enriquecimiento ocurre automáticamente al llamar a POST /api/v1/invoice/create.import { createInvoice } from "@openubl/sdk";import { zInvoice } from "@openubl/sdk/zod.gen";
const invoice = zInvoice.parse({serie: "F001",numero: 2,tipoOperacion: "0101",moneda: "PEN",proveedor: { ruc: "20100066603", razonSocial: "Mi Empresa S.A.C." },cliente: { tipoDocumentoIdentidad: "1", numeroDocumentoIdentidad: "12345678", nombre: "Cliente Ejemplo" },detalles: [ { descripcion: "Gravado", cantidad: 1, precio: 100, unidadMedida: "NIU", tipoAfectacionIGV: "10" }, { descripcion: "Exonerado", cantidad: 1, precio: 50, unidadMedida: "NIU", tipoAfectacionIGV: "20" },],});
const { data, error } = await createInvoice({body: invoice,});
if (error) throw new Error(JSON.stringify(error));
console.log(data.detalles[0].igv); // 18.00console.log(data.detalles[1].igv); // 0.00console.log(data.igvTotal); // 18.00# El enriquecimiento ocurre automáticamente al llamar a POST /api/v1/invoice/create.curl -X POST http://localhost:8000/api/v1/invoice/create -H "Content-Type: application/json" -d '{ "serie": "F001", "numero": 2, "tipoOperacion": "0101", "moneda": "PEN", "proveedor": { "ruc": "20100066603", "razonSocial": "Mi Empresa S.A.C." }, "cliente": { "tipoDocumentoIdentidad": "1", "numeroDocumentoIdentidad": "12345678", "nombre": "Cliente Ejemplo" }, "detalles": [ { "descripcion": "Gravado", "cantidad": 1, "precio": 100, "unidadMedida": "NIU", "tipoAfectacionIGV": "10" }, { "descripcion": "Exonerado", "cantidad": 1, "precio": 50, "unidadMedida": "NIU", "tipoAfectacionIGV": "20" } ]}'Notas importantes
Sección titulada «Notas importantes»- El enriquecimiento es idempotente: si un campo ya tiene valor,
ContentEnricherno lo sobrescribe. Puedes proporcionar totales manualmente cuando lo necesites. - Los documentos tipo
VoidedDocumentssolo recibenfechaEmisionautomática; sus comprobantes internos ya deben existir. - Si usas la API REST de openUBL, el enriquecimiento suele aplicarse automáticamente antes de la validación y el renderizado, salvo que la ruta indique lo contrario.