PDFlibPas, losLabs Delphi-PDF-bibliotek, kontrollerar affärsreglerna i EN 16931, XRechnung 3.0.2 och Peppol BIS Billing 3.0 direkt i Pascal. TPDFlib.ValidateInvoiceBusinessRulesBuiltIn läser XML:en inbäddad i en Factur-X- eller ZUGFeRD-PDF och returnerar en JSON-rapport med officiella regel-ID:n, utan Java-runtime, Saxon eller en Schematron-verktygskedja. Rapporten betecknar sig själv som en delmängd, och den beteckningen är dess viktigaste fält
Pressen bakom det här är mestadels tysk. Sedan januari 2025 måste varje inhemskt företag i Tyskland kunna ta emot strukturerade B2B-e-fakturor, och utfärdandetjänsten kommer stegvis. Helt plötsligt måste mottagarsidan i ett Delphi-baserat bokförings- eller ERP-system svara på en fråga den aldrig ställt sig förut: är XML:en inuti den här PDF:en egentligen en giltig EN 16931-faktura? Det officiella svaret kommer från Schematron-artefakter kompilerade till XSLT 2.0, eller från KoSIT-validatorn för XRechnung, och båda kör på JVM. Att skicka med och patcha en JRE bredvid varje desktopinstallation bara för att avvisa en trasig faktura är en kostnad många team helst slipper
Vad kontrollerar den inbyggda validatorn egentligen?
Den nativa validatorn mappar fakturans XML på EN 16931:s affärstermer och kör en dokumenterad delmängd av reglerna från EN 16931:s valideringsartefakter 1.3.16, där varje fynd redovisas under dess officiella regel-ID. Båda syntaxerna stöds: UN/CEFACT CII D16B, som är vad Factur-X och ZUGFeRD bäddar in, och OASIS UBL 2.1 Invoice och CreditNote. De täckta områdena är:
- Obligatoriska termer: specifikationsidentifierare, fakturanummer, utfärdandedatum, typkod, valuta, säljar- och köparnamn, adresser, dokumentsummor, momsredovisning och rader (BR-01 till BR-16, BR-21 till BR-28, BR-CO-04, BR-CO-18, BR-CO-26, BR-45 till BR-48)
- Beräkningar: rad-, avdrags-, avgifts-, moms- och betalningssummor, plus den skattepliktiga summan och momsbeloppet för varje momskategori (BR-CO-10 till BR-CO-17, BR-xx-08, BR-xx-09)
- Momskategoriregler för S, Z, E, AE, K, G, O, L och M: radsatser, undantagsorsaker och momsidentifierarna varje kategori kräver
- Alla kodlistor BR-CL-01 till BR-CL-26, inklusive de över 2100 UN/ECE Rec 20/21-enhetkoderna bakom BR-CL-23, och tvådecimalreglerna BR-DEC-01, -05, -09 till -20 och -23
- Profilregler: XRechnung 3.0.2 (BR-DE-1 till BR-DE-31, BR-DEX-01 till BR-DEX-14) på CII och UBL, och Peppol BIS Billing 3.0.20 core-, common-, kodlist- och nationalregler på UBL
Omfånget redovisas, inte antyds. Utan schemafil bekräftar schemasteget att dokumentet är välformadt och att rotelementet stämmer. Varje steg listar reglerna utanför delmängden i ett notCovered-fält, och GetBuiltInInvoiceRuleCatalog returnerar hela mappen som JSON, så ditt UI kan visa en användare exakt vad ett grönt resultat täcker
En gräns till är lätt att missa. Factur-X MINIMUM- och BASIC WL-dokument är inte EN 16931-fakturor, så validatorn tillämpar inte EN 16931-regler på dem. I stället lägger den till ett informativt BuiltIn.Coverage.Profile-fynd som säger det, vilket är det ärliga svaret och också en ledtråd: enligt de tyska reglerna räknas de två profilerna ändå inte som e-fakturor
Validera XML:en i en hybrid-PDF
ValidateInvoiceBusinessRulesBuiltIn tar ett argument, namnet på den inbäddade filen, och en tom sträng är det normala valet. PDFlibPas plockar då bilagan som Factur-X-detekteringen identifierar; för en PDF utan Factur-X-metadata faller den tillbaka till den enda .xml-bilagan. Flera kandidat-.xml-bilagor, eller två bilagor med det begärda namnet, ger ett Invoice.AttachmentAmbiguous-fel i stället för en gissning, avkodningen takas vid 64 MB, och en saknad faktura sätter LastErrorCode till 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]);
// Tomt namn: validera den upptäckta Factur-X-/ZUGFeRD-bilagan
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')); // aldrig True här
finally
Report.Free;
end;
finally
PDF.Free;
end;
end;
Toppnivån i rapporten bär på standard, version, profile, complete, valid, fullConformance, coverage, ett schema- och ett schematron-steg, samt ett package-objekt som är null i inbyggt läge. Varje steg redovisar en status, motorn, antalet exekverade regler, sin täckning och en issues-array. Varje issue har ruleID, fieldPath, severity, category, line och message
Läs fynd per kategori i stället för per meddelande
category-fältet är det som gör rapporten användbar i en verklig inkorg, för det talar om vem som måste fixa problemet. Värdena är syntax, schema, cardinality, calculation, vat, codelist, decimal, profile, coverage, processing och integrity. Ett beräkningsfynd pekar oftast på avsändarens avrundningskod, ett kodlistfynd på stamdata, och ett profilfynd på ett XRechnung-fält som köparreferensen
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; // täckningsnotiser som 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;
Kör samma loop över schema.issues också, eftersom välformadhets- och rotelementfel landar där och affärsregelsteget aldrig ser ett dokument det inte kunde parsa
Varför kan samma faktura klara i CII och falla i UBL?
De officiella artefakterna tillämpar inte samma numeriska tolerans i båda syntaxerna, och PDFlibPas kopierar varje regel från sin egen syntaxs XSLT i stället för att harmonisera dem. BR-CO-17 är det tydligaste fallet. Den kontrollerar att momskategorins momsbelopp är lika med skattepliktig summa gånger sats, avrundat till två decimaler, och båda syntaxerna tillåter en skillnad på en valutanhet. CII-artefakten skriver det som <= och >=; UBL-artefakten använder abs med en strikt <
Ta en skattepliktig summa på 100.00 till 19 procent, så den förväntade momsen är 19.00, och en faktura som deklarerar 20.00. I CII är 20.00 minus 1 lika med 19.00, vilket uppfyller <=, och BR-CO-17 klarar sig. I UBL är samma jämförelse strikt, 19.00 är inte mindre än 19.00, och BR-CO-17 faller. Ingen skickar den fakturan med flit, men generatorer med en avrundningsbugg sitter precis på sådana gränser, och en testsvit som bara täcker en syntax märker ingenting
Skillnaderna går längre än BR-CO-17. BR-xx-08 kräver exakt likhet efter avrundning till två decimaler i CII, medan UBL tillåter ±1 och bara slår till när kategorin faktiskt har rader, avdrag eller avgifter. BR-O-08 är exakt i båda. CII-artefakten definierar BR-AF-09 och BR-AG-09 som true(), så de faller aldrig där, medan UBL kontrollerar dem med ±1. Och när en CII-momsredovisning saknar sats helt går de icke-O-kategorireglerna igenom vakant, eftersom XPath every $rate över en tom sekvens är sant. Den nativa koden måste hoppa över dem på samma sätt, annars rapporterar den fel på officiella exempelfiler
En andra fälla gäller regel-ID:n. Regelns kategoriprefix är inte momskategorikoden: K mappar till BR-IC, L till BR-AF och M till BR-AG. Att härleda ID:t från kategorikoden ger BR-K-09 och BR-K-10, ID:n som inte finns i EN 16931. Om din applikation mappar regel-ID:n till hjälptexter, ta ID:t från rapporten och bygg det aldrig från momskategorikoden
Hur slås XRechnung- och Peppolreglerna på?
Den inbyggda validatorn väljer profilregler utifrån specifikationsidentifieraren (BT-24). En identifierare som innehåller xrechnung kör XRechnung 3.0.2:s CIUS-regler på CII eller UBL; tilläggsidentifieraren urn:xeinkauf.de:kosit:extension:xrechnung_3.0 slår dessutom på BR-DEX-reglerna. Kontrollerna av delradssummor och moms (BR-DEX-02, -03 och -09 till -14) kör bara på UBL; på CII rapporterar BR-DEX-15 att delrader inte är tillgängliga. En identifierare som innehåller peppol kör Peppol BIS Billing 3.0.20-reglerna, som Peppol bara definierar för UBL. Ett CII-dokument som utger sig för att ha en Peppol-customization får en informativ BuiltIn.Coverage.Profile-notis om att de reglerna inte kontrollerades, i stället för ett tyst godkännande. För andra CIUS-profiler, använd de officiella artefakterna
En UBL-detalj är värd att känna till: BR-06 och BR-07 accepterar bara PartyLegalEntity/RegistrationName som säljar- eller köparnamn. Att falla tillbaka till PartyName/Name ser snällt ut men gömmer riktiga BR-06-fel, vilket är varför kontrollen är strikt
När behövs den officiella Schematron fortfarande?
Använd de officiella artefakterna, EN 16931:s Schematron via Saxon eller KoSIT-validatorn för XRechnung, närhelst ett resultat har juridisk eller kontraktuell vikt: att avvisa en leverantörsfaktura, certifiera din egen generator, eller att avgöra en tvist. Den inbyggda rapporten sätter aldrig fullConformance till true. Flaggan kräver ett verifierat regelpaket, båda steg godkända, båda steg körda av en motor som exekverar paketets egna regler, och ingenting kvar i notCovered
ValidateInvoiceBusinessRulesWithPackage kör den vägen. Ett regelpaket är en katalog med ett rulepackage.xml-manifest, skrivet av CreateInvoiceRulePackage, som namnger standard, version, schema och Schematron-stylesheet och registrerar SHA-256 för varje fil. PDFlibPas kontrollerar varje digest när paketet laddas och igen precis innan en motor läser filen, vilket krymper fönstret för ett byte efter verifieringen. Sökvägar får inte lämna paketkatalogen, och den nativa XSD-laddaren vägrar importer som manifestet inte listar. Med xmllint konfigurerat läser xmllint själv de importerade schemana, så bara kontrollen vid laddning skyddar dem
const
// SHA-256 för den rulepackage.xml du granskat och godkänt
ApprovedManifestSHA256 = '<64 hex digits from your release review>';
var
Report, Pkg: TJSONObject;
begin
// PDF: en TPDFlib med fakturan redan laddad, som i första exemplet
Report := TJSONObject.ParseJSONValue(
PDF.ValidateInvoiceBusinessRulesWithPackage('',
'C:\Rules\EN16931-CII-1.3.16', ApprovedManifestSHA256,
'', // tomt: nativt XSD-steg
'C:\Java\bin\java.exe', 'C:\Saxon\saxon-he.jar',
0)) as TJSONObject; // 0: 30000 ms per verktyg
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;
Den låsta digesten är ingen valfri utsmyckning. Fildigests bevisar att filerna stämmer med manifestet, men ingenting inuti ett paket bevisar att manifestet självt är äkta: en angripare som kan skriva om reglerna kan skriva om manifestet i samma veva. Bara en digest som anroparen låst fast, i kod eller i signerad konfiguration, stänger det hålet, och rapporten håller pinned och verified isär så att du kan se vilken garanti du faktiskt har
Skickar du en tom JavaExecutable validerar schemasteget ändå nativt mot paketets XSD, inklusive facets och identity constraints sedan v3.672.0, men affärsreglerna faller tillbaka på den inbyggda delmängden och fullConformance förblir false. Schemakonstrukter som den nativa laddaren inte stöder, som substitution groups, får laddningen att falla med XSD.Unsupported i stället för att hoppas över
Var veraPDF och Mustang passar in
Ingen av de här funktionerna tittar på PDF-containern. PDF/A-3-överensstämmelse enligt ISO 19005-3, Factur-X:s XMP-tilläggschema och kopplingen associated file är separata frågor, och veraPDF och Mustang förblir referensverktygen för dem, enligt att validera e-fakturor med veraPDF och Mustang. Den producerande sidan, från att bifoga XML:en till att deklarera profilen, tas upp i att bygga Factur-X- och ZUGFeRD-hybridfakturor i Delphi och PDF/A-3-tilläggscheman för Factur-X-metadata
Ett förnuftigt upplägg kör den inbyggda kontrollen i processen på varje inkommande faktura, där den kostar millisekunder: en UBL-faktura på cirka 1 MB med base64-bilagor valideras på 15 ms, eller 79 ms med UBL 2.1:s XSD. Saxon-körningen i paketet hamnar i CI och i avvisningsvägen, och veraPDF täcker containern. Oavsett det inbyggda resultatet beskriver det den täckta delmängden, inte standarden, vilket är precis vad coverage-fältet finns för att påminna dig om
Fakturavalideringsfunktionerna, regelkatalogen och regelpaketverktygen följer med Delphi PDF Library för Delphi och C++Builder, tillsammans med API:erna för Factur-X, PDF/A-3 och inbäddade filer som används ovan