Teknisk artikel

Native EN 16931-validering av fakturor i Delphi utan Java

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

Pipeline för inbyggd EN 16931-fakturavalidering i PDF-biblioteket PDFlibPas för Delphi: Factur-X-detektering, schema- och schematronsteg och en JSON-rapport över täckt delmängd
den inbyggda validatorn kör schema- och schematronstegen i processen och redovisar exakt vilka regler delmängden täcker och vilka den inte täcker

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

Jämförelse av BR-CO-17-tolerans i PDFlibPas fakturavalidering för Delphi: samma faktura med en enhets avrundningsskillnad klarar CII och faller i UBL
en avrundningsenhet ligger exakt på BR-CO-17-gränsen, så samma faktura klarar CII och faller i UBL

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

Integritetsflöde för ett PDFlibPas-regelpaket för fakturor i Delphi: manifestets SHA-256-digests kontrolleras vid laddning och före varje läsning, låsta av anroparen
fildigests skyddar paketinnehållet, och bara en digest som anroparen låst skyddar manifestet självt

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