PDFlibPas, la Delphi PDF Library di losLab, verifica le business rule di EN 16931, XRechnung 3.0.2 e Peppol BIS Billing 3.0 nativamente in Pascal. TPDFlib.ValidateInvoiceBusinessRulesBuiltIn legge l'XML incorporato in un PDF Factur-X o ZUGFeRD e restituisce un report JSON con gli ID ufficiali delle regole, senza un runtime Java, senza Saxon e senza una toolchain Schematron. Il report si etichetta da solo come subset, e quell'etichetta è il campo più importante che contiene
La pressione dietro tutto questo viene soprattutto dalla Germania. Da gennaio 2025 ogni azienda nazionale tedesca deve poter ricevere e-fatture B2B strutturate, e l'obbligo di emissione arriva a scaglioni. Di colpo il lato ricezione di una applicazione Delphi di contabilità o ERP deve rispondere a una domanda che non si era mai posta: l'XML dentro questo PDF è davvero una fattura EN 16931 valida? La risposta ufficiale arriva da artefatti Schematron compilati in XSLT 2.0, o dal validatore KoSIT per XRechnung, e entrambi girano sulla JVM. Spedire e patchare una JRE accanto a ogni installazione desktop solo per scartare una fattura sbagliata è un costo che molti team preferirebbero non sostenere
Cosa verifica davvero il validatore integrato?
Il validatore nativo mappa l'XML della fattura sui termini business di EN 16931 ed esegue un subset documentato delle regole degli artefatti di validazione EN 16931 1.3.16, riportando ogni riscontro sotto il suo ID di regola ufficiale. Sono supportate entrambe le sintassi: UN/CEFACT CII D16B, quella che Factur-X e ZUGFeRD incorporano, e OASIS UBL 2.1 Invoice e CreditNote. Le aree coperte sono:
- Termini obbligatori: identificatore di specifica, numero fattura, data di emissione, codice tipo, valuta, nomi di venditore e acquirente, indirizzi, totali documento, scomposizione IVA e righe (BR-01 a BR-16, BR-21 a BR-28, BR-CO-04, BR-CO-18, BR-CO-26, BR-45 a BR-48)
- Calcoli: totali di riga, di sconto, di spesa, d'imposta e da pagare, più imponibile e imposta di ogni categoria IVA (BR-CO-10 a BR-CO-17, BR-xx-08, BR-xx-09)
- Regole di categoria IVA per S, Z, E, AE, K, G, O, L e M: aliquote di riga, motivi di esenzione e identificatori IVA che ogni categoria richiede
- Tutti gli elenchi codici BR-CL-01 a BR-CL-26, inclusi i più di 2100 codici unità UN/ECE Rec 20/21 dietro BR-CL-23, e le regole sui due decimali BR-DEC-01, -05, -09 a -20 e -23
- Regole di profilo: XRechnung 3.0.2 (BR-DE-1 a BR-DE-31, BR-DEX-01 a BR-DEX-14) su CII e UBL, e regole core, comuni, di elenchi codici e nazionali di Peppol BIS Billing 3.0.20 su UBL
Il perimetro viene dichiarato, non lasciato implicito. Senza un file schema, lo stadio schema conferma la buona formazione e l'elemento radice. Ogni stadio elenca le regole fuori dal subset in un campo notCovered, e GetBuiltInInvoiceRuleCatalog restituisce l'intera mappa come JSON, così la vostra UI può mostrare a un utente esattamente cosa copre un esito verde
Un altro confine è facile da perdere. I documenti Factur-X MINIMUM e BASIC WL non sono fatture EN 16931, quindi il validatore non applica loro le regole EN 16931. Aggiunge invece una segnalazione informativa BuiltIn.Coverage.Profile che lo dichiara, che è la risposta onesta e anche un indizio: secondo le regole tedesche quei due profili comunque non contano come e-fatture
Validare l'XML incorporato di un PDF ibrido
ValidateInvoiceBusinessRulesBuiltIn prende un argomento, il nome del file incorporato, e la stringa vuota è la scelta normale. PDFlibPas a quel punto prende l'allegato che il rilevamento Factur-X identifica; per un PDF senza metadati Factur-X ripiega sull'unico allegato .xml. Diversi allegati .xml candidati, o due allegati con il nome richiesto, producono un errore Invoice.AttachmentAmbiguous invece di un'ipotesi, la decodifica è limitata a 64 MB, e una fattura mancante porta LastErrorCode a 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 vuoto: valida l'allegato Factur-X / ZUGFeRD rilevato
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')); // "subset"
Writeln('full: ', Report.GetValue<Boolean>('fullConformance')); // qui mai True
finally
Report.Free;
end;
finally
PDF.Free;
end;
end;
Il primo livello del report contiene standard, version, profile, complete, valid, fullConformance, coverage, uno stadio schema e uno schematron, e un oggetto package che in modalità integrata è null. Ogni stadio riporta uno stato, il motore, il numero di regole eseguite, la sua copertura e un array issues. Ogni segnalazione ha ruleID, fieldPath, severity, category, line e message
Leggere le segnalazioni per categoria invece che per messaggio
Il campo category è ciò che rende il report utilizzabile in una casella reale, perché dice a chi spetta sistemare il problema. I valori sono syntax, schema, cardinality, calculation, vat, codelist, decimal, profile, coverage, processing e integrity. Un riscontro di calcolo di solito punta sul codice di arrotondamento del mittente, uno di codelist sui dati anagrafici, uno di profilo su un campo XRechnung come il buyer reference
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; // note di copertura come 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;
Eseguite lo stesso ciclo anche su schema.issues, dato che i fallimenti di buona formazione e di elemento radice finiscono lì e lo stadio delle business rule non vede mai un documento che non è riuscito a fare il parsing
Perché la stessa fattura può passare in CII e fallire in UBL?
Gli artefatti ufficiali non applicano la stessa tolleranza numerica nelle due sintassi, e PDFlibPas copia ogni regola dall'XSLT della propria sintassi invece di armonizzarle. BR-CO-17 è il caso più chiaro. Verifica che l'imposta della categoria IVA sia uguale a imponibile per aliquota, arrotondata a due decimali, ed entrambe le sintassi consentono una differenza di una unità di valuta. L'artefatto CII la scrive come <= e >=; l'artefatto UBL usa abs con un < severo
Prendete un imponibile di 100.00 al 19 percento, quindi imposta attesa 19.00, e una fattura che dichiara 20.00. In CII, 20.00 meno 1 fa 19.00, che soddisfa <=, e BR-CO-17 passa. In UBL lo stesso confronto è severo, 19.00 non è minore di 19.00, e BR-CO-17 fallisce. Nessuno spedisce quella fattura di proposito, ma i generatori con un bug di arrotondamento si stanno esattamente su confini del genere, e una suite di test che copre una sola sintassi non se ne accorge
Le differenze vanno oltre BR-CO-17. BR-xx-08 in CII esige uguaglianza esatta dopo l'arrotondamento a due decimali, mentre UBL concede ±1 e scatta solo quando la categoria ha davvero righe, sconti o spese. BR-O-08 è esatto in entrambe. L'artefatto CII definisce BR-AF-09 e BR-AG-09 come true(), quindi lì non falliscono mai, mentre UBL li verifica con ±1. E quando una scomposizione IVA CII non ha del tutto un'aliquota, le regole di categoria non-O passano per vacuità, perché un XPath every $rate su una sequenza vuota è vero. Il codice nativo deve saltarle allo stesso modo, oppure riporta errori sui file di esempio ufficiali
Una seconda trappola riguarda gli ID delle regole. Il prefisso di categoria della regola non è il codice di categoria IVA: K mappa su BR-IC, L su BR-AF e M su BR-AG. Derivare l'ID dal codice di categoria produce BR-K-09 e BR-K-10, ID che in EN 16931 non esistono. Se la vostra applicazione mappa gli ID delle regole su testi di aiuto, prendete l'ID dal report e non costruitelo mai dal codice della categoria d'imposta
Come vengono attivate le regole XRechnung e Peppol?
Il validatore integrato seleziona le regole di profilo dall'identificatore di specifica (BT-24). Un identificatore che contiene xrechnung esegue le regole CIUS di XRechnung 3.0.2 su CII o UBL; l'identificatore di estensione urn:xeinkauf.de:kosit:extension:xrechnung_3.0 abilita in più le regole BR-DEX. I controlli su sub invoice line amount e IVA (BR-DEX-02, -03 e -09 a -14) girano solo su UBL; su CII, BR-DEX-15 riporta che le sub invoice line non sono disponibili. Un identificatore che contiene peppol esegue le regole di Peppol BIS Billing 3.0.20, che Peppol definisce solo per UBL. Un documento CII che dichiara una personalizzazione Peppol riceve una nota informativa BuiltIn.Coverage.Profile che dice che quelle regole non sono state verificate, invece di un passaggio silenzioso. Per gli altri profili CIUS, usate gli artefatti ufficiali
Un dettaglio UBL vale la pena conoscere: BR-06 e BR-07 accettano solo PartyLegalEntity/RegistrationName come nome del venditore o dell'acquirente. Ripiegare su PartyName/Name sembra indulgente ma nasconde veri fallimenti di BR-06, ed è per questo che il controllo è severo
Quando serve ancora lo Schematron ufficiale?
Usate gli artefatti ufficiali, lo Schematron EN 16931 tramite Saxon o il validatore KoSIT per XRechnung, ogni volta che un esito ha peso legale o contrattuale: scartare una fattura di un fornitore, certificare il vostro generatore, o chiudere una disputa. Il report integrato non porta mai fullConformance a true. Quella bandiera richiede un rule package verificato, entrambi gli stadi superati, entrambi gli stadi eseguiti da un motore che esegue le regole del package stesso, e niente rimasto in notCovered
ValidateInvoiceBusinessRulesWithPackage percorre quella strada. Un rule package è una directory con un manifest rulepackage.xml, scritto da CreateInvoiceRulePackage, che nomina standard, versione, schema e stylesheet Schematron e registra lo SHA-256 di ogni file. PDFlibPas verifica ogni digest quando il package viene caricato e di nuovo subito prima che un motore legga il file, il che restringe la finestra per uno scambio dopo la verifica. I percorsi non possono uscire dalla directory del package, e il loader XSD nativo rifiuta gli import che il manifest non elenca. Con xmllint configurato, gli schema importati vengono letti da xmllint stesso, quindi solo il controllo al caricamento li protegge
const
// SHA-256 del rulepackage.xml che avete esaminato e approvato
ApprovedManifestSHA256 = '<64 hex digits from your release review>';
var
Report, Pkg: TJSONObject;
begin
// PDF: un TPDFlib con la fattura già caricata, come nel primo esempio
Report := TJSONObject.ParseJSONValue(
PDF.ValidateInvoiceBusinessRulesWithPackage('',
'C:\Rules\EN16931-CII-1.3.16', ApprovedManifestSHA256,
'', // vuoto: stadio XSD nativo
'C:\Java\bin\java.exe', 'C:\Saxon\saxon-he.jar',
0)) as TJSONObject; // 0: 30000 ms per strumento
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;
Il digest bloccato non è una decorazione opzionale. I digest dei file provano che i file corrispondono al manifest, ma nulla dentro un package prova che il manifest stesso sia autentico: un attaccante che può riscrivere le regole può riscrivere insieme anche il manifest. Solo un digest fissato da chi chiama, nel codice o in una configurazione firmata, chiude quel varco, e il report tiene pinned e verified separati così potete dire quale garanzia avete davvero
Se passate un JavaExecutable vuoto, lo stadio schema valida comunque nativamente contro l'XSD del package, facet e identity constraint inclusi dalla v3.672.0, ma le business rule ripiegano sul subset integrato e fullConformance resta false. I costrutti di schema che il loader nativo non supporta, come i substitution group, fanno fallire il caricamento con XSD.Unsupported invece di essere saltati
Dove stanno veraPDF e Mustang
Nessuna di queste funzioni guarda il contenitore PDF. La conformità PDF/A-3 secondo ISO 19005-3, lo schema di estensione XMP di Factur-X e la relazione associated-file sono questioni a parte, e veraPDF e Mustang restano gli strumenti di riferimento, come descritto in validazione di e-fatture con veraPDF e Mustang. Il lato produzione, dall'allegare l'XML alla dichiarazione del profilo, è trattato in costruzione di fatture ibride Factur-X e ZUGFeRD in Delphi e schemi di estensione PDF/A-3 per i metadati Factur-X
Una ripartizione sensata esegue il controllo integrato in-process su ogni fattura in ingresso, dove costa millisecondi: una fattura UBL di circa 1 MB con allegati base64 si valida in 15 ms, o 79 ms con l'XSD UBL 2.1. L'esecuzione Saxon pacchettizzata va in CI e nel percorso di scarto, e veraPDF copre il contenitore. Qualunque sia l'esito integrato, esso descrive il subset coperto, non lo standard, e il campo coverage è lì proprio per ricordarvelo
Le funzioni di validazione fatture, il catalogo regole e gli strumenti per i rule package viaggiano con la Delphi PDF Library per Delphi e C++Builder, insieme alle API Factur-X, PDF/A-3 e per file incorporati usate sopra