Technical Article

Facturas híbridas Factur-X y ZUGFeRD en Delphi

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 como página que lee un humano, y otra como XML Cross Industry Invoice legible por máquina almacenado dentro del archivo como archivo asociado. Las dos representaciones describen la misma factura. Esa naturaleza dual es todo el sentido de las familias de formatos que los mandatos europeos exigen ahora, 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 detalla cómo PDFlibPas ensambla una factura híbrida de este tipo en Delphi, dónde las normas dejan margen para equivocarse, y por qué un perfil del catálogo necesita un constructor XML completamente independiente

Qué es realmente una factura híbrida

La página visible y el XML incrustado sirven a lectores diferentes. Un empleado que aprueba un pago observa 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 contabiliza 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 todos ese modelo como un documento Cross Industry Invoice de UN/CEFACT, la sintaxis que transporta los campos de EN 16931 en tránsito

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 necesita ser un XML de factura. PDF/A-2 prohíbe incrustar archivos que no sean en sí mismos PDF/A, por lo que una factura Factur-X no puede ser PDF/A-2. La elección de PDF/A-3, por tanto, no es 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 norma ISO 32000 §7.11.4 define el flujo de archivo incrustado, el objeto que contiene el XML en bruto y sus parámetros. La parte que hace del archivo un archivo asociado válido es el §14.13, que añade el concepto de archivo asociado y la clave /AFRelationship. Esa clave establece cómo se relacionan los datos incrustados con el contenido al que se adjuntan, 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 añade información más allá de lo que muestra la página, un extra no contenido en la representación. 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 incluye con PDFlibPas impulsa la misma ruta de generación a través de seis perfiles, definidos como un array de registros en InvoiceModel.pas. Cada perfil contiene los valores que el escritor necesita: un nombre para mostrar, el nombre del archivo incrustado, un nivel de conformidad, la /AFRelationship, una versión, un código de país opcional, y el URN GuidelineID que el XML anuncia dentro de su contexto de 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 indica 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 cosmé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 rígidamente un único valor, que es la razón por la que existe el campo por perfil en lugar de una constante

La trampa de ZUGFeRD 1.0

Es tentador suponer que cada perfil es la Cross Industry Invoice EN 16931 con variaciones menores en la cantidad de campos opcionales que se rellenan. 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 cosmética

Los perfiles modernos emiten una Cross Industry Invoice de UN/CEFACT 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 del espacio de nombres difieren, el elemento raíz difiere, y el árbol de elementos difiere en todo momento: el esquema :1p0 agrupa los datos bajo ApplicableSupplyChainTradeAgreement, ApplicableSupplyChainTradeDelivery, y ApplicableSupplyChainTradeSettlement, mientras que :100 usa ApplicableHeaderTradeAgreement, ApplicableHeaderTradeDelivery, y ApplicableHeaderTradeSettlement. La nomenclatura es lo suficientemente similar para confundir y lo suficientemente diferente 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 condiciones de pago, no qué esquema los transporta. Por lo tanto, no se puede tomar un documento :100 y reetiquetarlo para ZUGFeRD 1.0. El ejemplo gestiona esto con una bandera en el registro de cada perfil y dos funciones de construcción separadas, seleccionando la correcta antes de que se genere ningún 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 exquisitez de implementación. Suministrar 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 tienen que ser construidas por código que sabe cuál de las dos está escribiendo

Seleccionar el 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 fiable. El modo 6 es PDF/A-3a, que añade los requisitos de estructura etiquetada y 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 la intención de salida sRGB integrada de 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, lo que es suficiente para una página visible fiel más el XML incrustado. Si necesita un perfil ICC explícito en lugar del integrado, LoadOutputIntentProfile lo intercambia después de establecer el modo. El ejemplo carga el perfil sRGB del repositorio de esta forma y recurre a la intención integrada cuando el archivo no es accesible, de modo que la intención de salida 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;

Construir 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 un tipo de fuente real en lugar de hacer referencia a una integrada

El adjunto es una sola llamada. AddFacturXAssociatedFileFromString toma los bytes XML sin procesar en UTF-8 más los metadatos del perfil, escribe el flujo del archivo incrustado, lo registra en el array /AF del catálogo que requiere PDF/A-3, aplica la /AFRelationship, y genera los metadatos de factura electrónica XMP que identifican el documento como Factur-X, ZUGFeRD o XRechnung. También comprueba que el ID de la pauta del XML coincida con el nivel de conformidad que usted solicitó, de forma que una discrepancia entre el XML que usted construyó y el perfil que usted nombró se detecta 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 un nombre de vendedor o comprador que no sea ASCII debe llegar a la llamada como octetos UTF-8 en bruto. Una conversión (cast) simple a través de la página de códigos ANSI del sistema corrompería esos caracteres y produciría en silencio una factura cuyo XML ya no coincide con su propia declaración. El ejemplo codifica a UTF-8 explícitamente antes de entregar los bytes, que es la forma segura de alimentar cualquier API de PDF orientada a bytes a partir de un string Unicode

Para adjuntar un XML que no sea un perfil de factura electrónica reconocido, AddPDFA3AssociatedFileFromString es la contrapartida genérica. Toma un nombre de archivo, un tipo MIME, una descripción, una relación, y los bytes, y escribe un archivo asociado PDF/A-3 simple sin ningún tipo de metadatos específicos de factura o comprobaciones de pautas. Úselo para datos suplementarios; use el método Factur-X para facturas, para que los metadatos del perfil y la coincidencia de pautas se escriban por usted

Una vez que se produce el documento, las siguientes preguntas son si pasa la validación de accesibilidad y PDF/A, y si puede firmarse sin romper el cumplimiento. Esas cuestiones se cubren en el recorrido sobre la comprobación previa (preflight) de PDF/A y PDF/UA y en el banco de trabajo de cumplimiento y firma. Todo esto se distribuye como parte de la Delphi PDF Library de PDFlibPas, junto con las API de propiedades del documento, etiquetado y PDF/A sobre las que se basa la ruta de la factura electrónica