Una factura Factur-X o ZUGFeRD son dos documentos llevando 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 contable de un comprador tiene que analizar frente a la norma EN 16931. El error que envía facturas rotas a producción es creer que conseguir el primero correctamente se obtiene el segundo gratis. No es así. Un archivo puede ser un PDF/A-3 impecable y aún así llevar un XML que ninguna autoridad fiscal aceptará, y puede llevar un XML de libro de texto EN 16931 dentro de un contenedor que falla la validación de archivo. Las dos 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 a una pregunta: ¿es esto un archivo PDF/A-3 conforme? Comprueba 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 y el nivel de conformidad correctos? Para una factura electrónica, también comprueba la fontanería de los archivos asociados que requiere PDF/A-3, porque el XML viaja como un archivo incrustado con una /AFRelationship y una entrada en el array /AF del catálogo del documento. veraPDF no dice nada sobre si el total de la factura cuadra, porque eso no está en su ámbito
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 para el perfil declarado y luego aplica las reglas de negocio EN 16931 y los conjuntos de reglas específicos de cada país superpuestos en la parte superior, el CIUS de XRechnung entre ellos. Comprueba que el identificador de IVA del vendedor esté presente cuando los totales lo exigen, que las cantidades de descuentos y cargos se concilien con el total del documento, que el URN de 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 dos herramientas es un superconjunto de la otra. veraPDF da por válido un contenedor estructuralmente perfecto alrededor de un XML sin sentido. Mustang da por válido 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, que es la razón por la que un arnés de validación serio ejecuta ambos y trata un archivo como enviablesolo cuando ambos coinciden
La matriz de validación
Para demostrar que la biblioteca produce archivos que sobreviven a ambas puertas, el arnés construye una matriz. Seis perfiles de facturas cubren el rango que una canalización europea 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 en función de dos subniveles de conformidad de PDF/A, 3b y 3u, porque los requisitos del nivel B y el nivel U divergen en el mapeo Unicode y un archivo que aprueba uno puede suspender el otro. Seis perfiles por dos niveles son doce archivos, todos y cada uno de ellos construidos de forma desatendida (headless) por la misma ruta de código que envía la muestra de GUI, por lo que los artefactos bajo prueba no están afinados 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 fontanería del contenedor era correcta en todos los aspectos: 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 división que el enfoque de dos herramientas existe para hacer aflorar. 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 en Mustang provinieron de dos causas distintas, y la corrección para cada uno es un detalle que vale la pena conocer antes de que usted mismo genere estos perfiles
El primero fue el perfil Factur-X EXTENDED France B2B. El generador original pasó una etiqueta interna como el nivel de conformidad y un URN interno como la pauta, y Mustang rechazó el archivo con un error de valor de conformidad no válido (invalid-conformance-value) seguido de un error de tipo de perfil no admitido (unsupported-profile-type). La razón es que el campo XMP fx:ConformanceLevel no es una ranura de texto libre para su propia denominación de perfil. 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 de 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 pauta dentro del XML, que tiene que llevar el prefijo urn:cen.eu:en16931:2017#conformant# que marca un CIUS conforme a la norma EN 16931. Pasar el valor estándar EXTENDED con FR como código de país y el URN de pauta correcto 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 pauta alineados. El argumento del nivel de conformidad lleva el token estándar, el argumento del código de país lleva FR, y el URN de pauta vive en los bytes XML que usted pasa
var
FileID: Integer;
begin
PDF.SetPDFAMode(5); // PDF/A-3b
PDF.NewDocument;
// ... draw the human-readable invoice page ...
// ExtendedXML carries an EN 16931 guideline URN of the form
// urn:cen.eu:en16931:2017#conformant#urn:factur-x.eu:1p0:extended
FileID := PDF.AddFacturXAssociatedFileFromString(
ExtendedXML,
'EXTENDED', // standard fx:ConformanceLevel, not an internal label
'factur-x.xml',
'Factur-X EXTENDED invoice',
'Alternative', // /AFRelationship
'1.0',
'FR'); // France B2B marked by country code, not by conformance
if FileID = 0 then
raise Exception.Create('Factur-X attachment rejected');
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, que es más estricto sobre la cardinalidad de lo que sugieren los resúmenes en prosa. El XSD requiere que la suma de liquidación de cabecera, ram:SpecifiedTradeSettlementMonetarySummation, contenga ram:ChargeTotalAmount y ram:AllowanceTotalAmount cada una exactamente una vez. El XML generado omitió ambos, por lo que Mustang informó que los elementos deben aparecer exactamente una vez. Estos no son opcionales cuando el esquema dice que minOccurs es uno. La emisión de ambos en el orden de secuencia XSD, inmediatamente después de ram:LineTotalAmount, con un valor de 0.00 cuando no hay cargos o descuentos, satisfizo el esquema. Un cero es un elemento presente; un elemento ausente es una violación del esquema. Con esas dos correcciones en su lugar, la matriz pasó a doce de doce en Mustang, mientras se mantuvo doce de doce en veraPDF
Los campos de XRechnung que cambian de inválido a válido
XRechnung merece su propia nota porque su CIUS alemán añade reglas de negocio que están ausentes en el conjunto base de la norma EN 16931, y fallan de formas que parecen indicar que no hay nada malo en el documento a simple vista. Dos de ellos se refieren a las 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 de conexión 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 trata como opcionales. XRechnung no. Omita cualquiera de ellos y la factura estará bien formada, será válida según el esquema y será rechazada
La tercera es la regla BR-DE-6, que exige que el número de teléfono de contacto del vendedor esté presente. Es el tipo de campo que un desarrollador elimina porque parece presentación en lugar de datos, y su ausencia produce un fallo de validación que apunta al grupo de contacto del vendedor en lugar de a cualquier cosa que falte de forma obvia. Proporcionar BT-34, BT-49 y el número de teléfono del vendedor es lo que mueve un archivo XRechnung de inválido a válido bajo Mustang, y nada de eso cambia nada de lo que ve veraPDF, porque los tres viven en el XML
Conectar la salida de la biblioteca a un validador
El punto arquitectónico detrás del arnés se generaliza a cualquier sistema de negocio. La biblioteca PDF escribe un contenedor conforme e incrusta 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 coherencia del contenedor, que el array /AF del catálogo, el árbol de nombres de archivos incrustados, el XMP DocumentFileName, el perfil, la pauta y la /AFRelationship concuerden, pero no valida códigos de impuestos ni concilia cantidades. La división correcta del trabajo es que el sistema empresarial extraiga el XML y lo entregue a un validador de facturas dedicado, exactamente de la misma manera que el arnés se lo entrega a Mustang
La lectura del archivo hacia atrás le indica lo que realmente se escribió. DetectFacturXInvoice informa de 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 XMP DocumentFileName, la etiqueta 5 el nivel de conformidad, la etiqueta 6 el identificador de la pauta y la etiqueta 7 la /AFRelationship. Confirmar que el nivel de conformidad que lee 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 salga de su compilació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); // XML guideline ID
Writeln('Profile: ', Profile);
Writeln('Guideline: ', Guideline);
// Hand the raw XML to a dedicated EN 16931 / Mustang validator.
Result := PDF.ExtractFacturXXMLToString;
end;
end;
ExtractFacturXXMLToString devuelve los bytes XML sin procesar como un AnsiString, listos para escribir en un archivo o transmitir en un proceso validador. En el arnés de prueba ese objetivo es Mustang, invocado a través de su jar de línea de comandos, con veraPDF ejecutado en la misma pasada sobre el mismo archivo. El cableado es pequeño: un generador de consola, EInvoiceValidation.dpr, escribe los doce archivos utilizando el modelo de factura compartida de la muestra, y un script, run-validation.ps1, conduce ambos validadores sobre el directorio de salida e imprime una tabla de aprobados y suspensos. La misma forma 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 en 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 firmar, la parte de preimpresión de este trabajo se cubre en nuestro recorrido sobre el preflight (comprobación previa) de PDF/A y PDF/UA en Delphi, y el flujo más amplio de certificación y posterior firma se describe en el banco de trabajo de cumplimiento y firma. Ambos se construyen 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í