Una factura Factur-X o ZUGFeRD consta de dos documentos compartiendo un mismo nombre de archivo. El documento exterior es un contenedor PDF/A-3 que un lector de archivo tiene que aceptar durante los próximos diez años. El documento interior es una factura XML que el sistema de contabilidad de un comprador tiene que analizar de acuerdo con la norma EN 16931. El error que envía facturas defectuosas a producción es creer que hacer el primero bien nos da el segundo gratis. No es así. Un archivo puede ser un PDF/A-3 perfecto y aun así llevar un XML que ninguna autoridad fiscal aceptará, y puede llevar un XML EN 16931 de libro de texto dentro de un contenedor que falla en la validación de archivo. Ambas capas son validadas por dos herramientas diferentes que no saben nada la una de la otra, y una canalización real tiene que satisfacer a ambas
Dos validadores, dos preguntas diferentes
veraPDF es la implementación de referencia para PDF/A. Apúntelo a una factura y responde una pregunta: es este un archivo PDF/A-3 conforme. Verifica las cosas que le importan a la norma ISO 19005-3. ¿Están incrustadas todas las fuentes? ¿Hay un OutputIntent? ¿Declaran los metadatos XMP la parte correcta y el nivel de conformidad? Para una factura electrónica, también verifica la estructura de archivos asociados que PDF/A-3 requiere, porque el XML viaja junto como un archivo incrustado con un /AFRelationship y una entrada en el arreglo /AF del catálogo del documento. veraPDF no dice nada sobre si el total de la factura suma bien, porque eso no está dentro de sus competencias
Mustang es el validador de código abierto del proyecto Mustangproject. Hace la pregunta ortogonal: es el XML incrustado una factura válida. Ejecuta el XML contra el esquema del perfil declarado y luego aplica las reglas de negocio de la norma EN 16931 y los conjuntos de reglas específicos del país superpuestos a esta, incluyendo la CIUS de XRechnung. Comprueba que el identificador de IVA del vendedor esté presente cuando los totales lo exigen, que los montos de cargos y descuentos cuadren con el total del documento, y que el URN del perfil en el XML coincida con lo que el archivo afirma ser. A Mustang no le importa si el PDF circundante incrusta sus fuentes, porque ese es el trabajo de veraPDF
Ninguna de las herramientas es un superconjunto de la otra. veraPDF pasa un contenedor estructuralmente perfecto alrededor de un XML sin sentido. Mustang pasa un XML perfecto envuelto en un contenedor al que le falta un OutputIntent. Cada uno detecta exactamente la clase de defecto a la que el otro es ciego, lo cual es la razón por la que un arnés de validación serio ejecuta ambos y trata a un archivo como listo para enviarse solo cuando ambos están de acuerdo
La matriz de validación
Para probar que la biblioteca produce archivos que superan ambas puertas, el arnés construye una matriz. Seis perfiles de facturación cubren el rango que una canalización europea se encuentra en la práctica: Factur-X EN 16931, Factur-X BASIC, la variante Factur-X EXTENDED France B2B, XRechnung 3.0, ZUGFeRD 1.0 COMFORT y ZUGFeRD 2.0 BASIC. Cada perfil se genera contra dos subniveles de conformidad PDF/A, 3b y 3u, debido a que los requisitos del nivel B y el nivel U difieren en el mapeo Unicode y un archivo que pasa uno puede fallar el otro. Seis perfiles por dos niveles equivalen a doce archivos, cada uno de ellos construido sin interfaz por la misma ruta de código que se incluye en el ejemplo de GUI, por lo que los artefactos bajo prueba no están ajustados a mano para la prueba
El generador escribe los doce y un script alimenta cada uno a ambos validadores. En la primera ejecución completa, veraPDF aprobó los doce. La estructura del contenedor era correcta en todos los casos: archivos asociados registrados, conformidad XMP declarada, intenciones de salida en su lugar. Mustang aprobó ocho. Cuatro facturas eran archivos PDF/A-3 estructuralmente válidos que llevaban un XML que el validador de reglas de negocio rechazó, que es precisamente la separación que el enfoque de dos herramientas busca revelar. Si el arnés hubiera confiado solo en veraPDF, esos cuatro habrían parecido terminados
Las dos correcciones que cerraron la brecha
Los cuatro fallos de Mustang provinieron de dos causas distintas, y vale la pena conocer la solución para cada una antes de generar estos perfiles usted mismo
La primera fue el perfil Factur-X EXTENDED France B2B. El generador original pasó una etiqueta interna como nivel de conformidad y un URN interno como directriz, y Mustang rechazó el archivo con un error de valor de conformidad no válido seguido por un error de tipo de perfil no admitido. La razón es que el campo XMP fx:ConformanceLevel no es un espacio de texto libre para su propia convención de nombres de perfiles. Factur-X define exactamente cinco valores estándar para ello: MINIMUM, BASIC WL, BASIC, EN 16931 y EXTENDED. Una factura B2B específica de Francia sigue siendo un documento con perfil EXTENDED en lo que respecta a los metadatos XMP. El carácter francés de la factura no se expresa inventando un sexto valor de conformidad. Se expresa mediante el código de país, FR, y mediante el identificador de la directriz dentro del XML, que tiene que llevar el prefijo urn:cen.eu:en16931:2017#conformant# que marca una CIUS conforme a la norma EN 16931. Pasar el valor estándar EXTENDED con FR como código de país y el URN correcto de la directriz hizo que el archivo fuera conforme
En la API de la biblioteca, eso es una llamada a AddFacturXAssociatedFileFromString con la conformidad, el país y la directriz alineados. El argumento de nivel de conformidad lleva el token estándar, el argumento del código de país lleva FR, y el URN de la directriz se encuentra en los bytes del XML que usted transmite
var
FileID: Integer;
begin
PDF.SetPDFAMode(5); // PDF/A-3b
PDF.NewDocument;
// ... dibuja la página de factura legible por humanos ...
// ExtendedXML lleva un URN de directriz EN 16931 con la forma
// urn:cen.eu:en16931:2017#conformant#urn:factur-x.eu:1p0:extended
FileID := PDF.AddFacturXAssociatedFileFromString(
ExtendedXML,
'EXTENDED', // fx:ConformanceLevel estándar, no una etiqueta interna
'factur-x.xml',
'Factur-X EXTENDED invoice',
'Alternative', // /AFRelationship
'1.0',
'FR'); // Francia B2B marcado por código de país, no por conformidad
if FileID = 0 then
raise Exception.Create('Adjunto Factur-X rechazado');
PDF.SaveToFile('02_Factur-X-EXTENDED-FR_PDFA-3b.pdf');
end;
La segunda causa fue el perfil ZUGFeRD 1.0 COMFORT, y no tuvo nada que ver con los metadatos. ZUGFeRD 1.0 se valida contra el XSD :1p0, el cual es más estricto con respecto a la cardinalidad de lo que sugieren los resúmenes en prosa. El XSD requiere que la suma monetaria de liquidación comercial del encabezado, ram:SpecifiedTradeSettlementMonetarySummation, contenga ram:ChargeTotalAmount y ram:AllowanceTotalAmount cada uno exactamente una vez. El XML generado omitió ambos, por lo que Mustang reportó que los elementos deben aparecer exactamente una vez. Estos no son opcionales cuando el esquema dice que minOccurs es uno. Emitir ambos en el orden de secuencia del XSD, inmediatamente después de ram:LineTotalAmount, con un valor de 0.00 cuando no hay cargos ni descuentos, satisfizo al esquema. Un cero es un elemento presente; un elemento ausente es una violación al esquema. Con esas dos correcciones en su lugar, la matriz llegó a doce de doce en Mustang, al tiempo que se mantenía en doce de doce en veraPDF
Los campos de XRechnung que cambian de no válido a válido
XRechnung merece su propia nota porque su CIUS alemana agrega reglas de negocio que están ausentes del conjunto base de la norma EN 16931, y fallan de formas en las que a simple vista parece no haber nada malo en el documento. Dos de ellas conciernen a direcciones electrónicas. BT-34 es la dirección electrónica del vendedor y BT-49 es la dirección electrónica del comprador, los puntos finales de enrutamiento que utiliza un portal del sector público alemán para entregar y acusar recibo de la factura. El modelo base EN 16931 los considera opcionales. XRechnung no. Si omite cualquiera de los dos, la factura está bien formada, es válida según el esquema, y es rechazada
La tercera es la regla BR-DE-6, que requiere que el número de teléfono de contacto del vendedor esté presente. Es el tipo de campo que un desarrollador abandona porque se siente como una presentación en lugar de datos, y su ausencia produce un fallo en la validación que apunta al grupo de contacto del vendedor en lugar de a cualquier cosa que obviamente falte. Suministrar BT-34, BT-49 y el número de teléfono del vendedor es lo que hace que un archivo XRechnung pase de ser no válido a ser válido bajo Mustang, y nada de eso cambia lo que ve veraPDF, porque los tres se encuentran dentro del XML
Conectar la salida de la biblioteca a un validador
El argumento arquitectónico detrás del arnés se generaliza a cualquier sistema de negocio. La biblioteca de PDF escribe un contenedor conforme y embebe el XML. No intenta, ni debería intentar, ser la autoridad en materia de reglas de negocio EN 16931. ValidateFacturXInvoice en la biblioteca comprueba la consistencia del contenedor, que el arreglo /AF del catálogo, el árbol de nombres de los archivos embebidos, el DocumentFileName de XMP, el perfil, la directriz y el /AFRelationship estén todos de acuerdo, pero no valida códigos de impuestos ni concilia montos. La división de trabajo correcta es que el sistema de negocio extraiga el XML y lo entregue a un validador de facturas dedicado, exactamente como el arnés se lo entrega a Mustang
Volver a leer el archivo le indica qué se escribió realmente. DetectFacturXInvoice informa si se reconoció una factura, y GetFacturXInvoiceInfo lee los campos de metadatos por etiqueta: la etiqueta 1 es el nombre del archivo incrustado, la etiqueta 2 el DocumentFileName XMP, la etiqueta 5 el nivel de conformidad, la etiqueta 6 el identificador de la directriz y la etiqueta 7 el /AFRelationship. Confirmar que el nivel de conformidad que lee de vuelta es el token estándar y no una etiqueta interna es la forma más barata de detectar el error EXTENDED antes de que un archivo abandone su proceso de construcción
function ExtractAndInspect(const PdfPath: string): AnsiString;
var
Profile, Guideline: WideString;
begin
Result := '';
PDF.LoadFromFile(PdfPath);
if PDF.DetectFacturXInvoice = 1 then
begin
Profile := PDF.GetFacturXInvoiceInfo(5); // fx:ConformanceLevel
Guideline := PDF.GetFacturXInvoiceInfo(6); // ID de la directriz XML
Writeln('Profile: ', Profile);
Writeln('Guideline: ', Guideline);
// Entrega el XML crudo a un validador dedicado EN 16931 / Mustang.
Result := PDF.ExtractFacturXXMLToString;
end;
end;
ExtractFacturXXMLToString devuelve los bytes de XML crudos como un AnsiString, listos para escribir a un archivo o transmitir en forma de flujo a un proceso validador. En el arnés de prueba, ese destino es Mustang, invocado a través de su jar de línea de comandos, con veraPDF ejecutándose en la misma pasada sobre el mismo archivo. Las conexiones son pocas: un generador de consola, EInvoiceValidation.dpr, escribe los doce archivos utilizando el modelo de factura compartido del ejemplo, y un script, run-validation.ps1, impulsa ambos validadores sobre el directorio de salida e imprime una tabla de aprobados y reprobados. La misma estructura de dos pasos, generar con la biblioteca y verificar con validadores externos, es lo que debería ejecutar un trabajo de integración continua en cada cambio que afecte la generación de facturas, porque la única forma de saber que un archivo satisface ambas capas es preguntar a ambas herramientas
Si su canalización también tiene que certificar el contenedor antes de la firma, el lado de preimpresión (preflight) de este trabajo se cubre en nuestro tutorial paso a paso sobre el preflight de PDF/A y PDF/UA en Delphi, y el flujo más amplio de certificar y luego firmar se describe en el entorno de trabajo de cumplimiento y firma. Ambos se basan en la misma ruta de generación que se envía como parte de la Delphi PDF Library para Delphi y C++Builder, junto con las API de PDF/A, archivos asociados y metadatos utilizadas aquí