Una factura electrónica conforme no es un PDF con un archivo XML grapado a un lado. Es un único documento PDF/A-3 que lleva la factura dos veces: una vez como una página que un humano lee y otra vez como un XML Cross Industry Invoice legible por máquina almacenado dentro del archivo como un archivo asociado. Las dos representaciones describen la misma factura. Esa naturaleza dual es el propósito de las familias de formatos que ahora exigen los mandatos europeos: Factur-X en Francia y Alemania, ZUGFeRD en los mercados de habla alemana y XRechnung para la facturación del sector público alemán. Este artículo explica cómo PDFlibPas ensambla una factura híbrida de este tipo en Delphi, dónde los estándares dejan margen para equivocarse y por qué un perfil en el catálogo necesita un generador XML completamente independiente
Qué es en realidad una factura híbrida
La página visible y el XML incrustado sirven a diferentes lectores. Un empleado que aprueba un pago mira la página renderizada. Un sistema de cuentas por pagar ingiere el XML, lee los totales y el desglose de impuestos como campos estructurados y registra el asiento sin que un humano teclee nada. El contenido semántico de ese XML se rige por la norma EN 16931, el estándar europeo que define el modelo de datos de la factura: qué campos existen, qué significan y cuáles son obligatorios. EN 16931 es un modelo semántico, no un formato de archivo. Factur-X, ZUGFeRD 2.x y XRechnung realizan ese modelo como un documento UN/CEFACT Cross Industry Invoice, la sintaxis que transmite los campos EN 16931 a través de la red
Para que el documento sea a la vez archivable y autodescriptivo, el contenedor es PDF/A-3, definido por la norma ISO 19005-3. PDF/A-3 es el nivel de conformidad que permite archivos incrustados arbitrarios, que es exactamente lo que debe ser un XML de factura. PDF/A-2 prohíbe la incrustación de archivos que no sean PDF/A, por lo que una factura Factur-X no puede ser PDF/A-2. La elección de PDF/A-3 no es, por lo tanto, una preferencia, es un requisito que se deriva directamente de querer incrustar datos que no son PDF en un documento de archivo
Por qué la relación es Alternative
Incrustar los bytes es la parte fácil. La ISO 32000 §7.11.4 define la transmisión de archivos incrustados, el objeto que contiene el XML en bruto y sus parámetros. La parte que hace que el archivo sea un archivo asociado válido es la §14.13, que agrega el concepto de un archivo asociado y la clave /AFRelationship. Esa clave establece cómo se relacionan los datos incrustados con el contenido al que están adjuntos, y el valor que exige Factur-X es Alternative
La elección importa porque los otros valores afirmarían algo falso sobre el documento. Source significaría que el XML es el material a partir del cual se generó el contenido visible, un maestro del que se deriva la página. Supplement significaría que el XML agrega información más allá de lo que muestra la página, un extra que no está contenido en el renderizado. Ninguna de las dos cosas es lo que es una factura Factur-X. El XML y la página son dos expresiones equivalentes de una misma factura, llevando el mismo contenido legal en dos formas. Alternative es el valor que dice exactamente eso: una representación alternativa equivalente del contenido visible. Un validador que lea cualquier otra relación en un archivo Factur-X lo rechazará, y con razón, porque la relación es una afirmación legible por máquina sobre para qué sirve el archivo adjunto
El catálogo de perfiles
El ejemplo de factura electrónica que se envía con PDFlibPas maneja la misma ruta de generación a través de seis perfiles, definidos como un arreglo de registros en InvoiceModel.pas. Cada perfil lleva los valores que necesita el escritor: un nombre para mostrar, el nombre del archivo incrustado, un nivel de conformidad, la relación /AFRelationship, una versión, un código de país opcional y el URN GuidelineID que el XML anuncia dentro del contexto de su documento
Los seis son Factur-X EN16931, Factur-X BASIC, Factur-X EXTENDED para Francia, XRechnung 3.0, ZUGFeRD 1.0 COMFORT y ZUGFeRD 2.0 BASIC. El GuidelineID es el campo que le dice a un receptor precisamente qué perfil esperar, y los valores son específicos. Factur-X EN16931 anuncia urn:cen.eu:en16931:2017. XRechnung 3.0 anuncia urn:cen.eu:en16931:2017#compliant#urn:xeinkauf.de:kosit:xrechnung_3.0. ZUGFeRD 2.0 BASIC anuncia urn:cen.eu:en16931:2017#compliant#urn:zugferd.de:2p0:basic. El nombre del archivo incrustado también es parte del contrato. Los perfiles Factur-X incrustan factur-x.xml, XRechnung incrusta xrechnung.xml, y los perfiles ZUGFeRD incrustan ZUGFeRD-invoice.xml o zugferd-invoice.xml. Un receptor escanea los nombres de los archivos adjuntos para encontrar la factura, por lo que el nombre del archivo no es algo estético
Vale la pena leer detenidamente un detalle del catálogo. La mayoría de los perfiles usan la relación Alternative, pero la entrada de XRechnung 3.0 en el ejemplo usa Source. Los dos formatos responden a diferentes validadores y convenciones, y el ejemplo establece la relación de cada perfil a partir del catálogo en lugar de codificar directamente un solo valor, por lo cual el campo por perfil existe en lugar de una constante
La trampa de ZUGFeRD 1.0
Es tentador asumir que cada perfil es el Cross Industry Invoice EN 16931 con variaciones menores en la cantidad de campos opcionales que usted llena. Eso es válido para cinco de los seis. No es válido para ZUGFeRD 1.0 COMFORT, y la razón es estructural más que estética
Los perfiles modernos emiten un UN/CEFACT Cross Industry Invoice con la versión de espacio de nombres :100, cuyo elemento raíz es rsm:CrossIndustryInvoice. ZUGFeRD 1.0 es anterior a ese esquema. Es el CrossIndustryDocument de 2014 con la versión de espacio de nombres :1p0, y su elemento raíz es rsm:CrossIndustryDocument. Los URN de los espacios de nombres difieren, el elemento raíz difiere y el árbol de elementos difiere en todas partes: el esquema :1p0 agrupa los datos en ApplicableSupplyChainTradeAgreement, ApplicableSupplyChainTradeDelivery y ApplicableSupplyChainTradeSettlement, donde :100 usa ApplicableHeaderTradeAgreement, ApplicableHeaderTradeDelivery y ApplicableHeaderTradeSettlement. Los nombres son lo suficientemente similares para engañar y lo suficientemente diferentes para romperse
La palabra COMFORT en el nombre del perfil describe cuán ricos son los datos, un perfil de grado de automatización con líneas de artículos completas, desglose de impuestos y términos de pago, no qué esquema lo contiene. Así que usted no puede tomar un documento :100 y reetiquetarlo para ZUGFeRD 1.0. El ejemplo maneja esto con una bandera en cada registro de perfil y dos funciones de construcción separadas, seleccionando la correcta antes de que se genere cualquier XML
function BuildInvoiceXMLText(const AProfile: TeInvoiceProfile;
const Data: TInvoiceData): string;
begin
// XMLFamily = 1 means the legacy ZUGFeRD 1.0 :1p0 schema; every
// other profile is the modern UN/CEFACT :100 Cross Industry Invoice.
if AProfile.XMLFamily = 1 then
Result := BuildZUGFeRD1Text(AProfile, Data)
else
Result := BuildCII100Text(AProfile, Data);
end;
La división no es una sutileza de la implementación. Alimentar un árbol :100 a un receptor ZUGFeRD 1.0 produce un documento que falla la validación del esquema en el elemento raíz, por lo que las dos familias deben ser construidas por un código que sepa cuál está escribiendo
Selección del nivel de PDF/A-3
PDF/A-3 tiene tres niveles de conformidad, y PDFlibPas los selecciona a través de SetPDFAMode. El modo 5 es PDF/A-3b, el nivel que garantiza una reproducción visual confiable. El modo 6 es PDF/A-3a, que agrega la estructura etiquetada y los requisitos de accesibilidad del nivel a. El modo 7 es PDF/A-3u, que requiere que todo el texto esté mapeado a Unicode. Habilitar el modo también incrusta el output intent sRGB incorporado en la biblioteca, la caracterización de color que exige PDF/A para que el color renderizado esté definido en lugar de depender del dispositivo
La mayoría de los flujos de facturas se ejecutan en 3b, que es suficiente para una página visible fiel más el XML incrustado. Si necesita un perfil ICC explícito en lugar del incorporado, LoadOutputIntentProfile lo intercambia después de que se establece el modo. El ejemplo carga el perfil sRGB del repositorio de esta manera y recurre al intent incorporado cuando el archivo no está disponible, por lo que el output intent siempre está presente
PDF := TPDFlib.Create;
try
// Mode 5 = PDF/A-3b, 6 = PDF/A-3a, 7 = PDF/A-3u.
if PDF.SetPDFAMode(5) <> 1 then
raise Exception.Create('PDF/A-3 mode could not be enabled');
// Optional: swap the built-in sRGB intent for an explicit ICC profile.
if PDF.LoadOutputIntentProfile(ICCFile, 'DeviceRGB') <> 1 then
{ fall back to the built-in sRGB intent that SetPDFAMode embedded };
finally
// ... continue building the document
end;
Construcción de la factura híbrida
Con el contenedor configurado, el resto son tres pasos en orden: establecer el modo PDF/A-3, dibujar la página legible por humanos y luego adjuntar el XML como un archivo asociado. La página visible es contenido ordinario. La única restricción que vale la pena recordar es que PDF/A prohíbe las 14 fuentes estándar no incrustadas, por lo que la página debe incrustar una familia tipográfica real en lugar de hacer referencia a una incorporada
El archivo adjunto es una sola llamada. AddFacturXAssociatedFileFromString toma los bytes XML UTF-8 en bruto más los metadatos del perfil, escribe la transmisión del archivo incrustado, la registra en el arreglo /AF del catálogo que requiere PDF/A-3, aplica la relación /AFRelationship y genera los metadatos de la factura electrónica XMP que identifican el documento como Factur-X, ZUGFeRD o XRechnung. También comprueba que el ID de la directriz del XML coincida con el nivel de conformidad que usted solicitó, de modo que una discrepancia entre el XML que usted construyó y el perfil que usted nombró se detecte en lugar de enviarse silenciosamente
// 1. PDF/A-3 mode and output intent are already set.
// 2. Draw the visible page (embeds a real TrueType font).
DrawInvoicePage(PDF, AProfile, Data);
// 3. Build the profile-correct XML and attach it as an
// associated file with /AFRelationship = Alternative.
InvoiceXML := BuildInvoiceXML(AProfile, Data); // AnsiString of UTF-8 bytes
FileID := PDF.AddFacturXAssociatedFileFromString(
InvoiceXML,
AProfile.ConformanceLevel, // e.g. 'EN16931'
AProfile.FileName, // 'factur-x.xml'
AProfile.Description,
AProfile.Relationship, // 'Alternative'
AProfile.Version, // '1.0'
AProfile.CountryCode); // '' or 'DE' or 'FR'
if FileID <= 0 then
raise Exception.Create('Invoice XML could not be attached');
PDF.SaveToFile(TargetFile);
Una sutileza en la ruta de datos es la codificación. El XML incrustado declara encoding="UTF-8", y el método toma sus bytes como un AnsiString, por lo que el nombre de un vendedor o comprador que no sea ASCII debe llegar a la llamada como octetos UTF-8 en bruto. Un cast simple a través de la página de códigos ANSI del sistema corrompería esos caracteres y produciría silenciosamente una factura cuyo XML ya no coincide con su propia declaración. El ejemplo codifica explícitamente a UTF-8 antes de entregar los bytes, que es la forma segura de alimentar cualquier API de PDF orientada a bytes desde un string Unicode
Para adjuntar un XML que no es un perfil de factura electrónica reconocido, AddPDFA3AssociatedFileFromString es la contraparte genérica. Toma un nombre de archivo, tipo MIME, descripción, relación y bytes, y escribe un archivo asociado PDF/A-3 simple sin ningún tipo de metadatos específicos de la factura o comprobaciones de directrices. Úselo para datos complementarios; use el método Factur-X para facturas, de modo que los metadatos del perfil y la coincidencia de las directrices se escriban por usted
Una vez que se produce el documento, las siguientes preguntas son si pasa la validación PDF/A y de accesibilidad, y si se puede firmar sin romper la conformidad. Eso está cubierto en el recorrido de preflight de PDF/A y PDF/UA y en el entorno de trabajo de cumplimiento y firma. Todo esto se incluye como parte de la Biblioteca PDF de Delphi PDFlibPas, junto con las API de PDF/A, etiquetado y propiedades de documento en las que se basa la ruta de la factura electrónica