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
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
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
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