Artículo técnico

Facturas híbridas Factur-X y ZUGFeRD en Delphi

Una factura electrónica que cumple con las normas no es un PDF con un archivo XML grapado a un lado. Es un solo documento PDF/A-3 que lleva la factura dos veces: una como una página que un humano puede leer, y otra como un XML de Cross Industry Invoice (Factura de Uso Cruzado por la Industria) legible por máquina almacenado dentro del archivo como un archivo asociado. Las dos representaciones describen la misma factura. Esa doble naturaleza es precisamente el objetivo de las familias de formatos que los mandatos europeos requieren ahora, Factur-X en Francia y Alemania, ZUGFeRD en todos los mercados de habla alemana y XRechnung para la facturación al sector público alemán. Este artículo detalla cómo PDFlibPas ensambla dicha factura híbrida en Delphi, en qué lugares los estándares dejan margen de error y por qué un perfil en el 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 distintos lectores. Un oficinista que aprueba un pago examina 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 asienta el registro sin que un humano deba teclear 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. La norma 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 de factura Cross Industry Invoice de UN/CEFACT, la sintaxis que transporta los campos de EN 16931 por la red

Para que el documento pueda archivarse y ser a la vez 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 un XML de factura necesita ser. PDF/A-2 prohíbe incrustar archivos que no sean ellos mismos PDF/A, por lo que una factura Factur-X no puede ser PDF/A-2. Por tanto, la elección de PDF/A-3 no es una preferencia, es un requisito que se deduce directamente del deseo de incrustar datos que no son PDF en un documento para ser archivado

Por qué la relación es Alternative

Incrustar los bytes es la parte fácil. ISO 32000 §7.11.4 define el flujo del archivo incrustado, el objeto que contiene el XML crudo y sus parámetros. La parte que hace que el archivo sea un archivo asociado válido es §14.13, que agrega el concepto de un archivo asociado y la clave /AFRelationship. Esa clave declara cómo los datos incrustados se relacionan con el contenido al que están adjuntos, y el valor que manda Factur-X es Alternative

La elección es importante porque los otros valores afirmarían algo falso sobre el documento. Source (Fuente) significaría que el XML es el material a partir del cual se generó el contenido visible, un original del que se deriva la página. Supplement (Suplemento) significaría que el XML agrega información más allá de lo que muestra la página, un extra no contenido en la representación. Ninguno de estos es lo que es una factura Factur-X. El XML y la página son dos expresiones equivalentes de una sola 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 declaración legible por máquina sobre el propósito del archivo adjunto

El catálogo de perfiles

El ejemplo de E-Invoice (Factura Electrónica) que viene con PDFlibPas controla la misma ruta de generación a través de seis perfiles, definidos como un arreglo de registros (records) 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, el /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 indica a un receptor precisamente qué perfil debe 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 de archivo incrustado es parte del contrato también. Los perfiles de Factur-X incrustan factur-x.xml, XRechnung incrusta xrechnung.xml, y los perfiles ZUGFeRD incrustan ZUGFeRD-invoice.xml o zugferd-invoice.xml. Un receptor examina los nombres de los adjuntos para encontrar la factura, de modo que el nombre del archivo no es algo meramente cosmético

Vale la pena leer detenidamente un detalle en el catálogo. La mayoría de los perfiles usan la relación Alternative, pero la entrada 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 desde el catálogo en lugar de codificar de manera rígida un solo valor, por lo que el campo por perfil existe en lugar de una constante

La trampa ZUGFeRD 1.0

Es tentador asumir que cada perfil es la Factura Cross Industry Invoice EN 16931 con variaciones menores en cuanto a cuántos campos opcionales rellena. Eso se cumple en cinco de los seis. No es válido para ZUGFeRD 1.0 COMFORT, y el motivo es estructural en lugar de cosmético

Los perfiles modernos emiten una Factura Cross Industry Invoice UN/CEFACT con la versión del espacio de nombres (namespace) :100, cuyo elemento raíz es rsm:CrossIndustryInvoice. ZUGFeRD 1.0 es anterior a ese esquema. Es el Documento CrossIndustryDocument de 2014 con la versión del espacio de nombres :1p0, y su elemento raíz es rsm:CrossIndustryDocument. Los URNs del espacio de nombres son distintos, el elemento raíz es distinto, y el árbol de elementos es distinto por doquier: el esquema :1p0 agrupa los datos bajo ApplicableSupplyChainTradeAgreement, ApplicableSupplyChainTradeDelivery y ApplicableSupplyChainTradeSettlement, donde :100 utiliza ApplicableHeaderTradeAgreement, ApplicableHeaderTradeDelivery y ApplicableHeaderTradeSettlement. Los nombres son lo bastante parecidos para engañar y lo bastante distintos como para causar fallos

La palabra COMFORT en el nombre del perfil describe qué tan ricos son los datos, siendo este un perfil de nivel de automatización con artículos de línea completos, desglose de impuestos y términos de pago, no especifica qué esquema los contiene. Por tanto, no puede tomar un documento :100 y reetiquetarlo para ZUGFeRD 1.0. El ejemplo aborda esto con un indicador en el registro de cada perfil y dos funciones de construcción independientes, seleccionando la correcta antes de generar cualquier XML

function BuildInvoiceXMLText(const AProfile: TeInvoiceProfile;
  const Data: TInvoiceData): string;
begin
  // XMLFamily = 1 significa el esquema legado ZUGFeRD 1.0 :1p0; cualquier
  // otro perfil es la moderna Cross Industry Invoice UN/CEFACT :100.
  if AProfile.XMLFamily = 1 then
    Result := BuildZUGFeRD1Text(AProfile, Data)
  else
    Result := BuildCII100Text(AProfile, Data);
end;

Esta división no es simplemente un detalle elegante de implementación. Alimentar un árbol :100 a un receptor ZUGFeRD 1.0 produce un documento que no supera la validación del esquema en el elemento raíz, por lo que las dos familias deben construirse con un código que sepa cuál está escribiendo

Selección del nivel PDF/A-3

PDF/A-3 cuenta con tres niveles de conformidad, y PDFlibPas los selecciona a través de SetPDFAMode. El Modo 5 es PDF/A-3b, el nivel que garantiza la reproducción visual confiable. El Modo 6 es PDF/A-3a, el cual agrega la estructura etiquetada y los requisitos de accesibilidad del nivel a. El Modo 7 es PDF/A-3u, el cual requiere que todo el texto sea mapeado a Unicode. Al activar el modo también se incrusta la intención de salida sRGB integrada en la biblioteca, la caracterización de color que exige PDF/A para que el color reproducido esté definido en lugar de depender del dispositivo

La mayor parte de los flujos de facturas funcionan en 3b, lo cual basta para una página visible fiel más el XML incrustado. Si se necesita un perfil ICC explícito en lugar del integrado, LoadOutputIntentProfile lo intercambia una vez que el modo ha sido configurado. El ejemplo carga el perfil sRGB del repositorio de este modo y recurre a la intención integrada cuando no se puede acceder al archivo, por lo 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('No se pudo habilitar el modo PDF/A-3');

  // Opcional: cambie la intención sRGB integrada por un perfil ICC explícito.
  if PDF.LoadOutputIntentProfile(ICCFile, 'DeviceRGB') <> 1 then
    { recurrir a la intención sRGB integrada que SetPDFAMode incrustó };
finally
  // ... continuar construyendo el documento
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 para el humano y, a continuación, adjuntar el XML como un archivo asociado. La página visible es un 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 letra real en lugar de hacer referencia a uno integrado

El adjunto es una sola llamada. AddFacturXAssociatedFileFromString toma los bytes XML en formato UTF-8 crudo más los metadatos del perfil, escribe el flujo del archivo incrustado, lo registra en el arreglo /AF del catálogo que requiere PDF/A-3, aplica el /AFRelationship, y genera los metadatos de factura electrónica XMP que identifican el documento como Factur-X, ZUGFeRD o XRechnung. Además, comprueba que el ID de la directriz del XML coincida con el nivel de conformidad solicitado, por lo que una discrepancia entre el XML que se ha creado y el perfil especificado se detecta a tiempo en lugar de ser enviada en silencio

// 1. El modo PDF/A-3 y la intención de salida ya están configurados.
// 2. Dibuja la página visible (incrusta una fuente TrueType real).
DrawInvoicePage(PDF, AProfile, Data);

// 3. Construye el XML correcto para el perfil y lo adjunta como un
//    archivo asociado con /AFRelationship = Alternative.
InvoiceXML := BuildInvoiceXML(AProfile, Data);   // AnsiString de bytes UTF-8
FileID := PDF.AddFacturXAssociatedFileFromString(
  InvoiceXML,
  AProfile.ConformanceLevel,   // p.ej. 'EN16931'
  AProfile.FileName,           // 'factur-x.xml'
  AProfile.Description,
  AProfile.Relationship,       // 'Alternative'
  AProfile.Version,            // '1.0'
  AProfile.CountryCode);       // '' o 'DE' o 'FR'
if FileID <= 0 then
  raise Exception.Create('No se pudo adjuntar el XML de la factura');

PDF.SaveToFile(TargetFile);

Una sutileza en la ruta de los 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 es ASCII debe llegar a la llamada como octetos UTF-8 sin procesar. Un "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 realiza la codificación a UTF-8 de forma explícita antes de pasar los bytes, la cual es la forma segura de alimentar cualquier API PDF orientada a bytes partiendo de un string Unicode

Para adjuntar XML que no sea un perfil de factura electrónica reconocido, su equivalente genérico es AddPDFA3AssociatedFileFromString. Toma un nombre de archivo, un tipo MIME, una descripción, una relación y bytes, y escribe un archivo asociado PDF/A-3 normal sin ningún tipo de comprobación de la directriz ni metadatos específicos de la factura. Úselo para datos suplementarios; utilice el método Factur-X para las facturas, de modo que los metadatos del perfil y la verificación de la directriz se escriban por usted

Una vez que el documento es producido, las siguientes preguntas son si pasa la validación PDF/A y la validación de accesibilidad, y si puede ser firmado sin quebrar el cumplimiento de normas. Éstas se abarcan en el tutorial paso a paso sobre el preflight de PDF/A y PDF/UA en Delphi y el entorno de trabajo de cumplimiento y firma. Todo esto se envía como parte de la PDFlibPas Delphi PDF Library, junto a las API de PDF/A, etiquetado y propiedades de documento en los cuales se basa la ruta de la factura electrónica