Artigo Técnico

Validação EN 16931 nativa em Delphi sem Java

O PDFlibPas, a PDF Library Delphi da losLab, verifica as regras de negócio EN 16931, XRechnung 3.0.2 e Peppol BIS Billing 3.0 nativamente em Pascal. O TPDFlib.ValidateInvoiceBusinessRulesBuiltIn lê o XML embutido num PDF Factur-X ou ZUGFeRD e devolve um relatório JSON com IDs de regras oficiais, sem runtime Java, sem Saxon e sem uma toolchain Schematron. O relatório rotula-se a si próprio como um subconjunto, e esse rótulo é o campo mais importante dele

Pipeline da validação incorporada de faturas EN 16931 na biblioteca PDF PDFlibPas para Delphi: deteção de Factur-X, etapas de schema e schematron e um relatório JSON de subconjunto coberto
o validador incorporado corre as etapas de schema e schematron no próprio processo e indica exatamente que regras o subconjunto cobre e que não cobre

A pressão por trás disto é maioritariamente alemã. Desde janeiro de 2025 que todos os negócios nacionais na Alemanha têm de estar aptos a receber faturas eletrónicas B2B estruturadas, e a obrigação de emitir segue por fases. De repente, o lado recetor de uma aplicação Delphi de contabilidade ou ERP tem de responder a uma pergunta que nunca antes lhe foi colocada: o XML dentro deste PDF é realmente uma fatura EN 16931 válida? A resposta oficial vem de artefactos Schematron compilados para XSLT 2.0, ou do validador KoSIT para XRechnung, e ambos correm na JVM. Distribuir e actualizar um JRE ao lado de cada instalação de desktop só para rejeitar uma fatura má é um custo que muitas equipas preferem não carregar

O que verifica realmente o validador incorporado?

O validador nativo mapeia o XML da fatura nos termos de negócio EN 16931 e corre um subconjunto documentado das regras dos artefactos de validação EN 16931 1.3.16, reportando cada achado sob o seu ID de regra oficial. Ambas as sintaxes são suportadas: UN/CEFACT CII D16B, que é o que o Factur-X e o ZUGFeRD embutem, e OASIS UBL 2.1 Invoice e CreditNote. As áreas cobertas são:

  • Termos obrigatórios: identificador de especificação, número de fatura, data de emissão, código de tipo, moeda, nomes do vendedor e do comprador, endereços, totais do documento, discriminação de IVA e linhas (BR-01 a BR-16, BR-21 a BR-28, BR-CO-04, BR-CO-18, BR-CO-26, BR-45 a BR-48)
  • Cálculos: totais de linha, desconto, encargo, imposto e a pagar, mais a base tributável e o montante de imposto de cada categoria de IVA (BR-CO-10 a BR-CO-17, BR-xx-08, BR-xx-09)
  • Regras de categoria de IVA para S, Z, E, AE, K, G, O, L e M: taxas de linha, motivos de isenção e os identificadores de IVA que cada categoria exige
  • Todas as listas de códigos BR-CL-01 a BR-CL-26, incluindo os mais de 2100 códigos de unidade UN/ECE Rec 20/21 por trás do BR-CL-23, e as regras de duas casas decimais BR-DEC-01, -05, -09 a -20 e -23
  • Regras de perfil: XRechnung 3.0.2 (BR-DE-1 a BR-DE-31, BR-DEX-01 a BR-DEX-14) em CII e UBL, e regras core, comuns, de listas de códigos e nacionais do Peppol BIS Billing 3.0.20 em UBL

O âmbito é reportado, não implícito. Sem um ficheiro de schema, a etapa de schema confirma a boa formação e o elemento raiz. Cada etapa lista as regras fora do subconjunto num campo notCovered, e o GetBuiltInInvoiceRuleCatalog devolve o mapa inteiro em JSON, para a sua UI poder mostrar a um utilizador exatamente o que um resultado verde cobre

Mais um limite fácil de perder. Os documentos Factur-X MINIMUM e BASIC WL não são faturas EN 16931, por isso o validador não lhes aplica regras EN 16931. Em vez disso acrescenta uma questão informativa BuiltIn.Coverage.Profile que o diz, o que é a resposta honesta e também uma pista: sob as regras alemãs esses dois perfis nem contam como faturas eletrónicas

Validar o XML embutido de um PDF híbrido

O ValidateInvoiceBusinessRulesBuiltIn recebe um argumento, o nome do ficheiro embutido, e uma string vazia é a escolha normal. O PDFlibPas escolhe então o anexo que a deteção Factur-X identifica; num PDF sem metadados Factur-X cai no único anexo .xml. Vários anexos .xml candidatos, ou dois anexos com o nome pedido, produzem um erro Invoice.AttachmentAmbiguous em vez de um palpite, a descodificação tem um teto de 64 MB, e uma fatura em falta põe o LastErrorCode em 401

uses
  System.SysUtils, System.JSON, PDFlibrary;

procedure CheckInboundInvoice(const FileName: string);
var
  PDF: TPDFlib;
  Report: TJSONObject;
begin
  PDF := TPDFlib.Create;
  try
    if PDF.LoadFromFile(FileName, '') <> 1 then
      raise Exception.CreateFmt('Cannot open %s (error %d)',
        [FileName, PDF.LastErrorCode]);
    // Nome vazio: validar o anexo Factur-X / ZUGFeRD detetado
    Report := TJSONObject.ParseJSONValue(
      PDF.ValidateInvoiceBusinessRulesBuiltIn('')) as TJSONObject;
    try
      Writeln('profile:  ', Report.GetValue<string>('profile'));
      Writeln('valid:    ', Report.GetValue<Boolean>('valid'));
      Writeln('coverage: ', Report.GetValue<string>('coverage'));        // "subconjunto"
      Writeln('full:     ', Report.GetValue<Boolean>('fullConformance')); // nunca True aqui
    finally
      Report.Free;
    end;
  finally
    PDF.Free;
  end;
end;

O nível de topo do relatório traz standard, version, profile, complete, valid, fullConformance, coverage, uma etapa schema e uma schematron, e um objeto package que é null no modo incorporado. Cada etapa reporta um estado, o motor, o número de regras executadas, a sua cobertura e um array issues. Cada questão tem ruleID, fieldPath, severity, category, line e message

Ler as questões por categoria em vez de por mensagem

O campo category é o que torna o relatório utilizável numa caixa de entrada real, porque diz quem tem de corrigir o problema. Os valores são syntax, schema, cardinality, calculation, vat, codelist, decimal, profile, coverage, processing e integrity. Um achado de calculation aponta geralmente para o código de arredondamento do emissor, um de codelist para os dados mestre, e um de profile para um campo XRechnung como a referência do comprador

procedure ListIssues(Report: TJSONObject);
var
  Item: TJSONValue;
  Issue: TJSONObject;
begin
  for Item in Report.GetValue<TJSONArray>('schematron.issues') do
  begin
    Issue := Item as TJSONObject;
    if Issue.GetValue<string>('severity') = 'info' then
      Continue;  // notas de cobertura como BuiltIn.Coverage.Profile
    Writeln(Format('[%s] %s at %s: %s', [
      Issue.GetValue<string>('category'),
      Issue.GetValue<string>('ruleID'),
      Issue.GetValue<string>('fieldPath'),
      Issue.GetValue<string>('message')]));
  end;
end;

Corra o mesmo loop também sobre schema.issues, porque as falhas de boa formação e de elemento raiz aí aterram e a etapa de regras de negócio nunca vê um documento que não conseguiu analisar

Porque pode a mesma fatura passar em CII e falhar em UBL?

Os artefactos oficiais não aplicam a mesma tolerância numérica nas duas sintaxes, e o PDFlibPas copia cada regra do XSLT da sua própria sintaxe em vez de as harmonizar. O BR-CO-17 é o caso mais claro. Verifica que o montante de imposto da categoria de IVA é igual à base tributável vezes a taxa, arredondado a duas casas decimais, e ambas as sintaxes admitem uma diferença de uma unidade de moeda. O artefacto CII escreve isso como <= e >=; o artefacto UBL usa abs com um < estrito

Pegue numa base tributável de 100.00 a 19 por cento, logo imposto esperado de 19.00, e numa fatura que declara 20.00. Em CII, 20.00 menos 1 é 19.00, o que satisfaz o <=, e o BR-CO-17 passa. Em UBL a mesma comparação é estrita, 19.00 não é menor que 19.00, e o BR-CO-17 falha. Ninguém envia essa fatura de propósito, mas geradores com um bug de arredondamento assentam precisamente em limites destes, e uma suite de testes que só cubra uma sintaxe não dá por isso

Comparação de tolerância do BR-CO-17 na validação de faturas do PDFlibPas para Delphi: a mesma fatura com uma diferença de arredondamento de uma unidade passa em CII e falha em UBL
uma unidade de arredondamento assenta exatamente no limite do BR-CO-17, por isso a mesma fatura passa em CII e falha em UBL

As diferenças vão mais longe que o BR-CO-17. O BR-xx-08 exige igualdade exata após arredondamento a duas casas decimais em CII, enquanto o UBL admite ±1 e só dispara quando a categoria tem de facto linhas, descontos ou encargos. O BR-O-08 é exato em ambos. O artefacto CII define o BR-AF-09 e o BR-AG-09 como true(), por isso nunca aí falham, enquanto o UBL os verifica com ±1. E quando uma discriminação de IVA CII não tem taxa nenhuma, as regras de categorias não-O passam por vacuidade, porque um XPath every $rate sobre uma sequência vazia é verdadeiro. O código nativo tem de as saltar da mesma forma, ou reporta erros em ficheiros de amostra oficiais

Uma segunda armadilha diz respeito aos IDs de regra. O prefixo de categoria da regra não é o código da categoria de IVA: K mapeia para BR-IC, L para BR-AF e M para BR-AG. Derivar o ID a partir do código de categoria produz BR-K-09 e BR-K-10, IDs que não existem no EN 16931. Se a sua aplicação mapeia IDs de regra para textos de ajuda, tire o ID do relatório e nunca o construa a partir do código da categoria de imposto

Como se ligam as regras XRechnung e Peppol?

O validador incorporado seleciona regras de perfil a partir do identificador de especificação (BT-24). Um identificador contendo xrechnung corre as regras CIUS XRechnung 3.0.2 em CII ou UBL; o identificador de extensão urn:xeinkauf.de:kosit:extension:xrechnung_3.0 liga adicionalmente as regras BR-DEX. As verificações de montante de linha de subfatura e de IVA (BR-DEX-02, -03 e -09 a -14) correm só em UBL; em CII, o BR-DEX-15 reporta que as linhas de subfatura não estão disponíveis. Um identificador contendo peppol corre as regras Peppol BIS Billing 3.0.20, que o Peppol define apenas para UBL. Um documento CII a reclamar uma customização Peppol recebe uma nota informativa BuiltIn.Coverage.Profile a dizer que aquelas regras não foram verificadas, em vez de uma passagem silenciosa. Para outros perfis CIUS, use os artefactos oficiais

Um detalhe UBL que vale a pena conhecer: o BR-06 e o BR-07 aceitam apenas PartyLegalEntity/RegistrationName como nome do vendedor ou do comprador. Recair em PartyName/Name parece tolerante mas esconde falhas reais do BR-06, e é por isso que a verificação é estrita

Quando é que ainda precisa do Schematron oficial?

Use os artefactos oficiais, o Schematron EN 16931 através do Saxon ou o validador KoSIT para XRechnung, sempre que um resultado tenha peso legal ou contratual: rejeitar a fatura de um fornecedor, certificar o seu próprio gerador, ou resolver uma disputa. O relatório incorporado nunca põe fullConformance a true. Essa flag exige um pacote de regras verificado, as duas etapas passadas, as duas etapas corridas por um motor que executa as regras próprias do pacote, e nada restante em notCovered

O ValidateInvoiceBusinessRulesWithPackage corre esse caminho. Um pacote de regras é um diretório com um manifesto rulepackage.xml, escrito pelo CreateInvoiceRulePackage, que nomeia o standard, a versão, o schema e a folha de estilos Schematron e registra o SHA-256 de cada ficheiro. O PDFlibPas verifica cada digest quando o pacote carrega e outra vez imediatamente antes de um motor ler o ficheiro, o que estreita a janela para uma troca após a verificação. Os caminhos não podem sair do diretório do pacote, e o loader XSD nativo recusa imports que o manifesto não liste. Com o xmllint configurado, os schemas importados são lidos pelo próprio xmllint, por isso só a verificação em tempo de carga os protege

const
  // SHA-256 do rulepackage.xml que reviu e aprovou
  ApprovedManifestSHA256 = '<64 hex digits from your release review>';
var
  Report, Pkg: TJSONObject;
begin
  // PDF: um TPDFlib com a fatura já carregada, como no primeiro exemplo
  Report := TJSONObject.ParseJSONValue(
    PDF.ValidateInvoiceBusinessRulesWithPackage('',
      'C:\Rules\EN16931-CII-1.3.16', ApprovedManifestSHA256,
      '',                          // vazio: etapa XSD nativa
      'C:\Java\bin\java.exe', 'C:\Saxon\saxon-he.jar',
      0)) as TJSONObject;          // 0: 30000 ms por ferramenta
  try
    if not (Report.FindValue('package') is TJSONObject) then
      raise Exception.Create('No rule package in the report');
    Pkg := Report.GetValue<TJSONObject>('package');
    if not (Pkg.GetValue<Boolean>('pinned') and Pkg.GetValue<Boolean>('verified')) then
      raise Exception.Create('Rule package failed its integrity check');
    if Report.GetValue<Boolean>('fullConformance') then
      Writeln('Conforms to the complete rule package');
  finally
    Report.Free;
  end;
end;

O digest fixado não é decoração opcional. Os digests de ficheiros provam que os ficheiros casam com o manifesto, mas nada dentro de um pacote prova que o próprio manifesto é genuíno: um atacante que consiga reescrever as regras consegue reescrever o manifesto junto com elas. Só um digest fixado pelo chamador, em código ou em configuração assinada, fecha essa lacuna, e o relatório mantém pinned e verified separados para que consiga dizer que garantia tem de facto

Fluxo de integridade de um pacote de regras de fatura PDFlibPas em Delphi: digests SHA-256 do manifesto reverificados na carga e antes de cada leitura, fixados pelo chamador
os digests de ficheiros protegem o conteúdo do pacote, e só um digest fixado pelo chamador protege o próprio manifesto

Se passar um JavaExecutable vazio, a etapa de schema ainda valida nativamente contra o XSD do pacote, incluindo facets e identity constraints desde a v3.672.0, mas as regras de negócio recaem no subconjunto incorporado e o fullConformance mantém-se false. Constructos de schema que o loader nativo não suporta, como substitution groups, fazem a carga falhar com XSD.Unsupported em vez de serem saltados

Onde entram o veraPDF e o Mustang

Nenhuma destas funções olha para o contentor PDF. A conformidade PDF/A-3 sob o ISO 19005-3, o extension schema XMP do Factur-X e a relação associated-file são questões separadas, e o veraPDF e o Mustang continuam a ser as ferramentas de referência para elas, como descrito em validar faturas eletrónicas com veraPDF e Mustang. O lado da produção, desde anexar o XML a declarar o perfil, está coberto em construir faturas híbridas Factur-X e ZUGFeRD em Delphi e extension schemas PDF/A-3 para metadados Factur-X

Uma divisão sensata corre a verificação incorporada in-process em cada fatura recebida, onde custa milissegundos: uma fatura UBL de cerca de 1 MB com anexos base64 valida em 15 ms, ou 79 ms com o XSD UBL 2.1. A corrida Saxon empacotada vai para o CI e para o caminho de rejeição, e o veraPDF cobre o contentor. Seja qual for o resultado incorporado, ele descreve o subconjunto coberto, não o standard, que é precisamente o que o campo coverage está lá para lhe lembrar

As funções de validação de faturas, o catálogo de regras e as ferramentas de pacote de regras viajam com a Delphi PDF Library para Delphi e C++Builder, juntamente com as APIs de Factur-X, PDF/A-3 e ficheiros embutidos usadas acima