Artículo técnico

Validación EN 16931 nativa en Delphi, sin Java

PDFlibPas, la PDF Library de losLab para Delphi, verifica reglas de negocio EN 16931, XRechnung 3.0.2 y Peppol BIS Billing 3.0 de forma nativa en Pascal. TPDFlib.ValidateInvoiceBusinessRulesBuiltIn lee el XML incrustado en un PDF Factur-X o ZUGFeRD y devuelve un reporte JSON con IDs de reglas oficiales, sin runtime de Java, sin Saxon y sin una cadena de herramientas Schematron. El reporte se etiqueta a sí mismo como un subconjunto, y esa etiqueta es el campo más importante que contiene

Pipeline de validación de facturas EN 16931 integrada en la biblioteca PDF PDFlibPas para Delphi: detección Factur-X, etapas schema y schematron, y un reporte JSON del subconjunto cubierto
el validador integrado ejecuta las etapas schema y schematron en proceso y reporta exactamente qué reglas cubre el subconjunto y cuáles no

La presión detrás de esto es mayormente alemana. Desde enero de 2025 todo negocio doméstico en Alemania debe poder recibir facturas electrónicas B2B estructuradas, y la obligación de emitirlas llega por etapas. De pronto, el lado receptor de una aplicación contable o ERP en Delphi tiene que responder una pregunta que nunca se hizo antes: ¿el XML dentro de este PDF es de verdad una factura EN 16931 válida? La respuesta oficial viene de artefactos Schematron compilados a XSLT 2.0, o del validador KoSIT para XRechnung, y ambos corren sobre la JVM. Distribuir y parchar un JRE junto a cada instalación de escritorio solo para rechazar una factura mala es un costo que muchos equipos prefieren no cargar

¿Qué verifica en realidad el validador integrado?

El validador nativo mapea el XML de la factura sobre los términos de negocio de EN 16931 y ejecuta un subconjunto documentado de las reglas de los artefactos de validación EN 16931 1.3.16, reportando cada hallazgo bajo su ID de regla oficial. Ambas sintaxis están soportadas: UN/CEFACT CII D16B, que es lo que incrustan Factur-X y ZUGFeRD, y OASIS UBL 2.1 Invoice y CreditNote. Las áreas cubiertas son:

  • Términos obligatorios: identificador de especificación, número de factura, fecha de emisión, código de tipo, moneda, nombres de vendedor y comprador, direcciones, totales del documento, desglose de IVA y líneas (BR-01 a BR-16, BR-21 a BR-28, BR-CO-04, BR-CO-18, BR-CO-26, BR-45 a BR-48)
  • Cálculos: totales de línea, descuentos, cargos, impuestos y a pagar, más la base imponible y el importe del IVA de cada categoría (BR-CO-10 a BR-CO-17, BR-xx-08, BR-xx-09)
  • Reglas de categoría de IVA para S, Z, E, AE, K, G, O, L y M: tasas por línea, razones de exención y los identificadores de IVA que cada categoría exige
  • Todas las listas de códigos BR-CL-01 a BR-CL-26, incluidos los más de 2100 códigos de unidad UN/ECE Rec 20/21 detrás de BR-CL-23, y las reglas de dos decimales BR-DEC-01, -05, -09 a -20 y -23
  • Reglas de perfil: XRechnung 3.0.2 (BR-DE-1 a BR-DE-31, BR-DEX-01 a BR-DEX-14) sobre CII y UBL, y las reglas core, comunes, de listas de códigos y nacionales de Peppol BIS Billing 3.0.20 sobre UBL

El alcance se reporta, no se da por sentado. Sin un archivo de esquema, la etapa de esquema confirma buena formación y el elemento raíz. Cada etapa lista las reglas fuera del subconjunto en un campo notCovered, y GetBuiltInInvoiceRuleCatalog devuelve el mapa completo como JSON, así que su UI puede mostrarle al usuario exactamente qué cubre un resultado verde

Otra frontera es fácil de pasar por alto. Los documentos Factur-X MINIMUM y BASIC WL no son facturas EN 16931, así que el validador no les aplica reglas EN 16931. En cambio agrega un hallazgo informativo BuiltIn.Coverage.Profile que lo dice, que es la respuesta honesta y también una pista: bajo las reglas alemanas esos dos perfiles de todos modos no cuentan como facturas electrónicas

Validar el XML incrustado de un PDF híbrido

ValidateInvoiceBusinessRulesBuiltIn recibe un argumento, el nombre del archivo incrustado, y una cadena vacía es la elección normal. PDFlibPas entonces elige el adjunto que la detección de Factur-X identifica; para un PDF sin metadatos Factur-X recurre al único adjunto .xml. Varios adjuntos .xml candidatos, o dos adjuntos con el nombre solicitado, producen un error Invoice.AttachmentAmbiguous en lugar de una adivinanza, la decodificación se limita a 64 MB, y una factura ausente pone LastErrorCode en 401

uses
  System.SysUtils, System.JSON, PDFlibrary;

procedure CheckInboundInvoice(const FileName: string);
var
  PDF: TPDFlib;
  Report: TJSONObject;
begin
  PDF := TPDFlib.Create;
  try
    if PDF.LoadFromFile(FileName, '') <> 1 then
      raise Exception.CreateFmt('Cannot open %s (error %d)',
        [FileName, PDF.LastErrorCode]);
    // Nombre vacío: valida el adjunto Factur-X / ZUGFeRD detectado
    Report := TJSONObject.ParseJSONValue(
      PDF.ValidateInvoiceBusinessRulesBuiltIn('')) as TJSONObject;
    try
      Writeln('profile:  ', Report.GetValue<string>('profile'));
      Writeln('valid:    ', Report.GetValue<Boolean>('valid'));
      Writeln('coverage: ', Report.GetValue<string>('coverage'));        // "subset"
      Writeln('full:     ', Report.GetValue<Boolean>('fullConformance')); // nunca True aquí
    finally
      Report.Free;
    end;
  finally
    PDF.Free;
  end;
end;

El nivel superior del reporte lleva standard, version, profile, complete, valid, fullConformance, coverage, una etapa schema y una schematron, y un objeto package que es null en modo integrado. Cada etapa reporta un estado, el motor, el número de reglas ejecutadas, su cobertura y un arreglo issues. Cada hallazgo tiene ruleID, fieldPath, severity, category, line y message

Leer hallazgos por categoría en vez de por mensaje

El campo category es lo que hace el reporte utilizable en una bandeja de entrada real, porque le dice quién tiene que arreglar el problema. Los valores son syntax, schema, cardinality, calculation, vat, codelist, decimal, profile, coverage, processing e integrity. Un hallazgo de cálculo suele apuntar al código de redondeo del emisor, uno de codelist a los datos maestros, y uno de profile a un campo XRechnung como la referencia del comprador

procedure ListIssues(Report: TJSONObject);
var
  Item: TJSONValue;
  Issue: TJSONObject;
begin
  for Item in Report.GetValue<TJSONArray>('schematron.issues') do
  begin
    Issue := Item as TJSONObject;
    if Issue.GetValue<string>('severity') = 'info' then
      Continue;  // notas de cobertura como BuiltIn.Coverage.Profile
    Writeln(Format('[%s] %s at %s: %s', [
      Issue.GetValue<string>('category'),
      Issue.GetValue<string>('ruleID'),
      Issue.GetValue<string>('fieldPath'),
      Issue.GetValue<string>('message')]));
  end;
end;

Ejecute el mismo bucle también sobre schema.issues, porque los fallos de buena formación y de elemento raíz aterrizan allí y la etapa de reglas de negocio nunca ve un documento que no pudo parsear

¿Por qué la misma factura puede pasar en CII y fallar en UBL?

Los artefactos oficiales no aplican la misma tolerancia numérica en ambas sintaxis, y PDFlibPas copia cada regla desde el XSLT de su propia sintaxis en vez de armonizarlas. BR-CO-17 es el caso más claro. Verifica que el importe del impuesto de la categoría de IVA iguale base imponible por tasa, redondeado a dos decimales, y ambas sintaxis permiten una diferencia de una unidad de moneda. El artefacto CII lo escribe como <= y >=; el artefacto UBL usa abs con un < estricto

Tome una base imponible de 100.00 al 19 por ciento, de modo que el impuesto esperado es 19.00, y una factura que declara 20.00. En CII, 20.00 menos 1 es igual a 19.00, lo que satisface <=, y BR-CO-17 pasa. En UBL la misma comparación es estricta, 19.00 no es menor que 19.00, y BR-CO-17 falla. Nadie envía esa factura a propósito, pero los generadores con un bug de redondeo se sientan exactamente sobre esos límites, y una suite de pruebas que solo cubre una sintaxis no se va a enterar

Comparación de la tolerancia BR-CO-17 en la validación de facturas PDFlibPas para Delphi: la misma factura con una diferencia de redondeo de una unidad pasa en CII y falla en UBL
una unidad de redondeo cae justo en la frontera de BR-CO-17, así que la misma factura pasa en CII y falla en UBL

Las diferencias van más allá de BR-CO-17. BR-xx-08 exige igualdad exacta tras redondear a dos decimales en CII, mientras que UBL permite ±1 y solo se dispara cuando la categoría realmente tiene líneas, descuentos o cargos. BR-O-08 es exacta en ambas. El artefacto CII define BR-AF-09 y BR-AG-09 como true(), así que nunca fallan allí, mientras que UBL las verifica con ±1. Y cuando un desglose de IVA CII no tiene tasa alguna, las reglas de categoría no-O pasan vacuamente porque un every $rate de XPath sobre una secuencia vacía es verdadero. El código nativo tiene que saltárselas de la misma forma, o reporta errores en archivos de muestra oficiales

Una segunda trampa concierne a los IDs de reglas. El prefijo de categoría de la regla no es el código de categoría de IVA: K mapea a BR-IC, L a BR-AF y M a BR-AG. Derivar el ID desde el código de categoría produce BR-K-09 y BR-K-10, IDs que no existen en EN 16931. Si su aplicación mapea IDs de reglas a textos de ayuda, tome el ID del reporte y nunca lo construya desde el código de categoría de impuestos

¿Cómo se activan las reglas de XRechnung y Peppol?

El validador integrado selecciona reglas de perfil a partir del identificador de especificación (BT-24). Un identificador que contiene xrechnung ejecuta las reglas CIUS de XRechnung 3.0.2 sobre CII o UBL; el identificador de extensión urn:xeinkauf.de:kosit:extension:xrechnung_3.0 además habilita las reglas BR-DEX. Las verificaciones de importe de línea de subfactura e IVA (BR-DEX-02, -03 y -09 a -14) corren solo sobre UBL; en CII, BR-DEX-15 reporta que las líneas de subfactura no están disponibles. Un identificador que contiene peppol ejecuta las reglas de Peppol BIS Billing 3.0.20, que Peppol define solo para UBL. Un documento CII que reclama una personalización Peppol recibe una nota informativa BuiltIn.Coverage.Profile que dice que esas reglas no se verificaron, en lugar de un pase silencioso. Para otros perfiles CIUS, use los artefactos oficiales

Un detalle de UBL vale la pena conocer: BR-06 y BR-07 aceptan solo PartyLegalEntity/RegistrationName como nombre del vendedor o comprador. Recurrir a PartyName/Name parece indulgente pero esconde fallos reales de BR-06, y por eso la verificación es estricta

¿Cuándo todavía necesita el Schematron oficial?

Use los artefactos oficiales, el Schematron de EN 16931 vía Saxon o el validador KoSIT para XRechnung, siempre que un resultado tenga peso legal o contractual: rechazar la factura de un proveedor, certificar su propio generador, o resolver una disputa. El reporte integrado nunca pone fullConformance en true. Esa bandera exige un paquete de reglas verificado, ambas etapas aprobadas, ambas etapas ejecutadas por un motor que corre las reglas propias del paquete, y nada pendiente en notCovered

ValidateInvoiceBusinessRulesWithPackage ejecuta ese camino. Un paquete de reglas es un directorio con un manifiesto rulepackage.xml, escrito por CreateInvoiceRulePackage, que nombra el estándar, la versión, el esquema y la hoja de estilo Schematron, y registra el SHA-256 de cada archivo. PDFlibPas verifica cada digest cuando el paquete carga y de nuevo justo antes de que un motor lea el archivo, lo que estrecha la ventana para un reemplazo tras la verificación. Las rutas no pueden salir del directorio del paquete, y el loader XSD nativo rechaza imports que el manifiesto no lista. Con xmllint configurado, los esquemas importados los lee el propio xmllint, así que solo la verificación al cargar los protege

const
  // SHA-256 del rulepackage.xml que usted revisó y aprobó
  ApprovedManifestSHA256 = '<64 hex digits from your release review>';
var
  Report, Pkg: TJSONObject;
begin
  // PDF: un TPDFlib con la factura ya cargada, como en el primer ejemplo
  Report := TJSONObject.ParseJSONValue(
    PDF.ValidateInvoiceBusinessRulesWithPackage('',
      'C:\Rules\EN16931-CII-1.3.16', ApprovedManifestSHA256,
      '',                          // vacío: etapa XSD nativa
      'C:\Java\bin\java.exe', 'C:\Saxon\saxon-he.jar',
      0)) as TJSONObject;          // 0: 30000 ms por herramienta
  try
    if not (Report.FindValue('package') is TJSONObject) then
      raise Exception.Create('No rule package in the report');
    Pkg := Report.GetValue<TJSONObject>('package');
    if not (Pkg.GetValue<Boolean>('pinned') and Pkg.GetValue<Boolean>('verified')) then
      raise Exception.Create('Rule package failed its integrity check');
    if Report.GetValue<Boolean>('fullConformance') then
      Writeln('Conforms to the complete rule package');
  finally
    Report.Free;
  end;
end;

El digest fijado no es un adorno opcional. Los digests de archivos prueban que los archivos coinciden con el manifiesto, pero nada dentro de un paquete prueba que el manifiesto mismo es genuino: un atacante que puede reescribir las reglas puede reescribir el manifiesto junto con ellas. Solo un digest fijado por quien llama, en código o en configuración firmada, cierra ese hueco, y el reporte mantiene pinned y verified separados para que pueda saber qué garantía tiene realmente

Flujo de integridad de un paquete de reglas de facturas PDFlibPas en Delphi: digests SHA-256 del manifiesto re-verificados al cargar y antes de cada lectura, fijados por quien llama
los digests de archivos protegen el contenido del paquete, y solo un digest fijado por quien llama protege el manifiesto mismo

Si pasa un JavaExecutable vacío, la etapa de esquema sigue validando de forma nativa contra el XSD del paquete, incluidos facets y restricciones de identidad desde v3.672.0, pero las reglas de negocio recurren al subconjunto integrado y fullConformance se queda en false. Los constructos de esquema que el loader nativo no soporta, como substitution groups, hacen que la carga falle con XSD.Unsupported en lugar de saltarse

Dónde encajan veraPDF y Mustang

Ninguna de estas funciones mira el contenedor PDF. La conformidad PDF/A-3 bajo ISO 19005-3, el esquema de extensión XMP de Factur-X y la relación de archivo asociado son preguntas aparte, y veraPDF y Mustang siguen siendo las herramientas de referencia para ellas, como se describe en validar facturas electrónicas con veraPDF y Mustang. El lado productor, desde adjuntar el XML hasta declarar el perfil, está cubierto en construir facturas híbridas Factur-X y ZUGFeRD en Delphi y esquemas de extensión PDF/A-3 para metadatos Factur-X

Una división sensata corre la verificación integrada en proceso sobre cada factura entrante, donde cuesta milisegundos: una factura UBL de alrededor de 1 MB con adjuntos base64 se valida en 15 ms, o 79 ms con el XSD de UBL 2.1. La ejecución empaquetada con Saxon va a CI y a la ruta de rechazo, y veraPDF cubre el contenedor. Sea cual sea el resultado integrado, describe el subconjunto cubierto, no el estándar, que es exactamente lo que el campo coverage está ahí para recordarle

Las funciones de validación de facturas, el catálogo de reglas y las herramientas de paquetes de reglas se distribuyen con la Delphi PDF Library para Delphi y C++Builder, junto con las APIs de Factur-X, PDF/A-3 y archivos incrustados usadas arriba