Artigo Técnico

Faturas Híbridas Factur-X e ZUGFeRD em Delphi

Uma fatura eletrónica conforme não é um PDF com um ficheiro XML anexado. É um único documento PDF/A-3 que transporta a fatura duas vezes: uma como página que um ser humano lê e outra como XML Cross Industry Invoice legível por máquina, armazenado dentro do ficheiro como ficheiro associado. As duas representações descrevem a mesma fatura. Essa natureza dual é o propósito central das famílias de formatos que os mandatos europeus agora exigem: Factur-X em França e Alemanha, ZUGFeRD nos mercados de língua alemã e XRechnung para faturação do setor público alemão. Este artigo explica como o PDFlibPas monta tal fatura híbrida em Delphi, onde as normas deixam margem para erros e por que razão um perfil no catálogo precisa de um construtor XML completamente separado

O que é efetivamente uma fatura híbrida

A página visível e o XML incorporado servem leitores diferentes. Um funcionário a aprovar um pagamento olha para a página renderizada. Um sistema de contas a pagar ingere o XML, lê os totais e a discriminação fiscal como campos estruturados e regista a entrada sem que um humano introduza nada. O conteúdo semântico desse XML é governado pela EN 16931, a norma europeia que define o modelo de dados de fatura: que campos existem, o que significam e quais são obrigatórios. A EN 16931 é um modelo semântico, não um formato de ficheiro. Factur-X, ZUGFeRD 2.x e XRechnung realizam todos esse modelo como documento Cross Industry Invoice da UN/CEFACT, a sintaxe que transporta os campos EN 16931

Para que o documento seja simultaneamente arquivável e autodescritivo, o contentor é PDF/A-3, definido pela ISO 19005-3. O PDF/A-3 é o nível de conformidade que permite ficheiros incorporados arbitrários, que é exatamente o que um XML de fatura necessita de ser. O PDF/A-2 proíbe a incorporação de ficheiros que não sejam eles próprios PDF/A, pelo que uma fatura Factur-X não pode ser PDF/A-2. A escolha do PDF/A-3 não é portanto uma preferência: é um requisito que resulta diretamente do desejo de incorporar dados não-PDF num documento de arquivo

Por que a relação é Alternative

Incorporar os bytes é a parte fácil. A ISO 32000 §7.11.4 define o fluxo de ficheiro incorporado, o objeto que contém o XML em bruto e os seus parâmetros. A parte que torna o ficheiro um ficheiro associado válido é a §14.13, que acrescenta o conceito de ficheiro associado e a chave /AFRelationship. Essa chave indica como os dados incorporados se relacionam com o conteúdo ao qual estão anexados, e o valor que o Factur-X exige é Alternative

A escolha é importante porque os outros valores afirmariam algo falso sobre o documento. Source significaria que o XML é o material a partir do qual o conteúdo visível foi gerado, um original de que a página deriva. Supplement significaria que o XML acrescenta informação além do que a página mostra, um extra não contido na renderização. Nenhum destes é o que uma fatura Factur-X é. O XML e a página são duas expressões equivalentes de uma fatura, transportando o mesmo conteúdo legal em duas formas. Alternative é o valor que afirma exatamente isso: uma representação alternativa equivalente do conteúdo visível. Um validador que leia qualquer outra relação num ficheiro Factur-X irá rejeitá-lo, e com razão, porque a relação é uma afirmação legível por máquina sobre o propósito do anexo

O catálogo de perfis

O exemplo de fatura eletrónica que acompanha o PDFlibPas percorre o mesmo caminho de geração com seis perfis, definidos como um array de registos em InvoiceModel.pas. Cada perfil contém os valores que o escritor necessita: um nome de apresentação, o nome do ficheiro incorporado, um nível de conformidade, o /AFRelationship, uma versão, um código de país opcional e o URN GuidelineID que o XML anuncia no contexto do documento

Os seis são: Factur-X EN16931, Factur-X BASIC, Factur-X EXTENDED para França, XRechnung 3.0, ZUGFeRD 1.0 COMFORT e ZUGFeRD 2.0 BASIC. O GuidelineID é o campo que indica ao recetor exatamente que perfil esperar, e os valores são específicos. O Factur-X EN16931 anuncia urn:cen.eu:en16931:2017. O XRechnung 3.0 anuncia urn:cen.eu:en16931:2017#compliant#urn:xeinkauf.de:kosit:xrechnung_3.0. O ZUGFeRD 2.0 BASIC anuncia urn:cen.eu:en16931:2017#compliant#urn:zugferd.de:2p0:basic. O nome do ficheiro incorporado também faz parte do contrato. Os perfis Factur-X incorporam factur-x.xml, o XRechnung incorpora xrechnung.xml e os perfis ZUGFeRD incorporam ZUGFeRD-invoice.xml ou zugferd-invoice.xml. Um recetor analisa os nomes dos anexos para encontrar a fatura, pelo que o nome do ficheiro não é cosmético

Um detalhe no catálogo merece atenção. A maioria dos perfis utiliza a relação Alternative, mas a entrada XRechnung 3.0 no exemplo usa Source. Os dois formatos respondem a validadores e convenções diferentes, e o exemplo define a relação de cada perfil a partir do catálogo em vez de codificar um único valor, o que explica a existência do campo por perfil em vez de uma constante

A armadilha do ZUGFeRD 1.0

É tentador assumir que cada perfil é o Cross Industry Invoice EN 16931 com variações menores no número de campos opcionais que se preenchem. Isso aplica-se a cinco dos seis. Não se aplica ao ZUGFeRD 1.0 COMFORT, e a razão é estrutural e não cosmética

Os perfis modernos emitem um Cross Industry Invoice da UN/CEFACT com versão de namespace :100, cujo elemento raiz é rsm:CrossIndustryInvoice. O ZUGFeRD 1.0 é anterior a esse esquema. É o CrossIndustryDocument de 2014 com versão de namespace :1p0, e o seu elemento raiz é rsm:CrossIndustryDocument. Os URNs de namespace diferem, o elemento raiz difere e a árvore de elementos difere em todo o lado: o esquema :1p0 agrupa dados sob ApplicableSupplyChainTradeAgreement, ApplicableSupplyChainTradeDelivery e ApplicableSupplyChainTradeSettlement, onde o :100 usa ApplicableHeaderTradeAgreement, ApplicableHeaderTradeDelivery e ApplicableHeaderTradeSettlement. A nomenclatura é suficientemente semelhante para enganar e suficientemente diferente para quebrar

A palavra COMFORT no nome do perfil descreve a riqueza dos dados, um perfil de nível de automação com itens de linha completos, discriminação de impostos e condições de pagamento, não o esquema que os transporta. Por isso não é possível pegar num documento :100 e reidentificá-lo para ZUGFeRD 1.0. O exemplo trata isto com um sinalizador em cada registo de perfil e duas funções construtoras separadas, selecionando a correta antes de qualquer XML ser gerado

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;

A separação não é uma conveniência de implementação. Alimentar uma árvore :100 a um recetor ZUGFeRD 1.0 produz um documento que falha a validação de esquema no elemento raiz, pelo que as duas famílias têm de ser construídas por código que sabe qual está a escrever

Selecionar o nível PDF/A-3

O PDF/A-3 tem três níveis de conformidade, e o PDFlibPas seleciona-os através de SetPDFAMode. O modo 5 é PDF/A-3b, o nível que garante reprodução visual fiável. O modo 6 é PDF/A-3a, que adiciona os requisitos de estrutura com etiquetas e acessibilidade do nível a. O modo 7 é PDF/A-3u, que exige que todo o texto seja mapeado para Unicode. Ativar o modo também incorpora o output intent sRGB incorporado na biblioteca, a caracterização de cor que o PDF/A exige para que a cor renderizada seja definida em vez de dependente do dispositivo

A maioria dos fluxos de faturas funciona em 3b, que é suficiente para uma página visível fiel mais o XML incorporado. Se precisar de um perfil ICC explícito em vez do incorporado, LoadOutputIntentProfile substitui-o após a definição do modo. O exemplo carrega o perfil sRGB do repositório desta forma e recorre ao intent incorporado quando o ficheiro não está disponível, pelo que o output intent está sempre 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 a fatura híbrida

Com o contentor configurado, o restante são três etapas por ordem: definir o modo PDF/A-3, desenhar a página legível por humanos e depois anexar o XML como ficheiro associado. A página visível é conteúdo normal. A única restrição a recordar é que o PDF/A proíbe as fontes Standard 14 não incorporadas, pelo que a página tem de incorporar uma face de fonte real em vez de referenciar uma incorporada

O anexo é uma única chamada. AddFacturXAssociatedFileFromString recebe os bytes XML em UTF-8 em bruto mais os metadados do perfil, escreve o fluxo de ficheiro incorporado, regista-o na matriz /AF do Catálogo que o PDF/A-3 exige, aplica o /AFRelationship e gera os metadados XMP de fatura eletrónica que identificam o documento como Factur-X, ZUGFeRD ou XRechnung. Também verifica que o ID de guia do XML corresponde ao nível de conformidade que pediu, pelo que uma discrepância entre o XML criado e o perfil nomeado é detetada em vez de enviada 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);

Uma subtileza no caminho de dados é a codificação. O XML incorporado declara encoding="UTF-8", e o método recebe os seus bytes como AnsiString, pelo que um nome de vendedor ou comprador não-ASCII tem de chegar à chamada como octetos UTF-8 em bruto. Uma conversão simples através da página de código ANSI do sistema corromperia esses caracteres e produziria silenciosamente uma fatura cujo XML já não corresponde à sua própria declaração. O exemplo codifica explicitamente para UTF-8 antes de entregar os bytes, que é a forma segura de alimentar qualquer API de PDF orientada a bytes a partir de uma string Unicode

Para anexar XML que não seja um perfil de fatura eletrónica reconhecido, AddPDFA3AssociatedFileFromString é a contrapartida genérica. Recebe um nome de ficheiro, tipo MIME, descrição, relação e bytes, e escreve um ficheiro associado PDF/A-3 simples sem metadados específicos de fatura ou verificações de guia. Use-o para dados complementares; use o método Factur-X para faturas, para que os metadados do perfil e a correspondência de guia sejam escritos por si

Uma vez produzido o documento, as próximas questões são se passa na validação de PDF/A e acessibilidade, e se pode ser assinado sem quebrar a conformidade. Esses temas são abordados em o guia de preflight PDF/A e PDF/UA e o banco de trabalho de conformidade e assinatura. Tudo isto é fornecido como parte da Biblioteca PDF PDFlibPas para Delphi, juntamente com as APIs de PDF/A, etiquetagem e propriedades de documentos sobre as quais o caminho de fatura eletrónica assenta