Artykuł techniczny

Natywna walidacja faktur EN 16931 w Delphi bez Javy

PDFlibPas, biblioteka PDF dla Delphi od losLab, sprawdza reguły biznesowe EN 16931, XRechnung 3.0.2 i Peppol BIS Billing 3.0 natywnie w Pascalu. TPDFlib.ValidateInvoiceBusinessRulesBuiltIn czyta XML osadzony w PDF-ie Factur-X albo ZUGFeRD i zwraca raport JSON z oficjalnymi identyfikatorami reguł, bez runtime Javy, bez Saxon i bez toolchainu Schematron. Raport sam siebie oznacza jako podzbiór i ta etykieta jest najważniejszym polem w środku

Pipeline wbudowanej walidacji faktur EN 16931 w bibliotece PDF PDFlibPas dla Delphi: wykrywanie Factur-X, etapy schema i schematron oraz raport JSON pokrytego podzbioru
wbudowany walidator uruchamia etapy schema i schematron w ramach procesu i raportuje dokładnie, które reguły podzbiór pokrywa, a których nie

Presja za tym jest w dużej mierze niemiecka. Od stycznia 2025 każdy krajowy biznes w Niemczech musi umieć przyjmować strukturalne e-faktury B2B, a obowiązek wystawiania nadchodzi etapami. Nagle strona przychodząca aplikacji księgowej albo ERP w Delphi musi odpowiedzieć na pytanie, o które wcześniej nikt nie pytał: czy XML wewnątrz tego PDF-a faktycznie jest poprawną fakturą EN 16931? Oficjalna odpowiedź pochodzi z artefaktów Schematron skompilowanych do XSLT 2.0 albo z walidatora KoSIT dla XRechnung, a oba działają na JVM. Rozprowadzanie i łatanie JRE obok każdej instalacji desktopowej tylko po to, żeby odrzucić złą fakturę, to koszt, którego wiele zespołów wolałoby nie dźwigać

Co naprawdę sprawdza wbudowany walidator?

Natywny walidator mapuje XML faktury na terminy biznesowe EN 16931 i uruchamia udokumentowany podzbiór reguł z artefaktów walidacyjnych EN 16931 1.3.16, zgłaszając każde znalezisko pod oficjalnym identyfikatorem reguły. Obie składnie są wspierane: UN/CEFACT CII D16B, którą osadzają Factur-X i ZUGFeRD, oraz OASIS UBL 2.1 Invoice i CreditNote. Pokryte obszary to:

  • Terminy obowiązkowe: identyfikator specyfikacji, numer faktury, data wystawienia, kod typu, waluta, nazwy sprzedawcy i nabywcy, adresy, sumy dokumentu, rozbicie VAT i pozycje (BR-01 do BR-16, BR-21 do BR-28, BR-CO-04, BR-CO-18, BR-CO-26, BR-45 do BR-48)
  • Wyliczenia: sumy pozycji, upustów, opłat, podatku i kwoty do zapłaty, plus baza i kwota podatku każdej kategorii VAT (BR-CO-10 do BR-CO-17, BR-xx-08, BR-xx-09)
  • Reguły kategorii VAT dla S, Z, E, AE, K, G, O, L i M: stawki pozycji, powody zwolnienia i identyfikatory VAT wymagane przez daną kategorię
  • Wszystkie listy kodowe BR-CL-01 do BR-CL-26, łącznie z ponad 2100 kodami jednostek UN/ECE Rec 20/21 za BR-CL-23, oraz reguły dwóch miejsc dziesiętnych BR-DEC-01, -05, -09 do -20 i -23
  • Reguły profili: XRechnung 3.0.2 (BR-DE-1 do BR-DE-31, BR-DEX-01 do BR-DEX-14) na CII i UBL oraz reguły core, common, code list i narodowe Peppol BIS Billing 3.0.20 na UBL

Zakres jest raportowany, nie sugerowany. Bez pliku schema etap schema potwierdza poprawność składniową i element główny. Każdy etap wymienia reguły spoza podzbioru w polu notCovered, a GetBuiltInInvoiceRuleCatalog zwraca całą mapę jako JSON, więc twój UI może pokazać użytkownikowi dokładnie, co pokrywa zielony wynik

Jeszcze jedna granica łatwo umyka. Dokumenty Factur-X MINIMUM i BASIC WL nie są fakturami EN 16931, więc walidator nie stosuje do nich reguł EN 16931. Zamiast tego dodaje informacyjny problem BuiltIn.Coverage.Profile, który to mówi — to uczciwa odpowiedź, a przy okazji podpowiedź: według reguł niemieckich te dwa profile i tak nie liczą się jako e-faktury

Walidacja osadzonego XML-a hybrydowego PDF-a

ValidateInvoiceBusinessRulesBuiltIn bierze jeden argument, nazwę osadzonego pliku, i pusty string to normalny wybór. PDFlibPas wybiera wtedy załącznik wskazany przez wykrywanie Factur-X; dla PDF-a bez metadanych Factur-X spada na jedyny załącznik .xml. Kilka kandydujących załączników .xml albo dwa załączniki o żądanej nazwie dają błąd Invoice.AttachmentAmbiguous zamiast zgadywania, dekodowanie ma limit 64 MB, a brakująca faktura ustawia LastErrorCode na 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]);
    // Pusta nazwa: waliduj wykryty załącznik Factur-X / ZUGFeRD
    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')); // tu nigdy True
    finally
      Report.Free;
    end;
  finally
    PDF.Free;
  end;
end;

Szczyt raportu niesie standard, version, profile, complete, valid, fullConformance, coverage, etapy schema i schematron oraz obiekt package, który w trybie wbudowanym jest nullem. Każdy etap raportuje status, silnik, liczbę wykonanych reguł, swoje pokrycie i tablicę issues. Każdy problem ma ruleID, fieldPath, severity, category, line i message

Czytanie problemów po kategorii zamiast po treści

Pole category robi raport użytecznym w prawdziwej skrzynce, bo mówi, kto ma problem naprawić. Wartości to syntax, schema, cardinality, calculation, vat, codelist, decimal, profile, coverage, processing i integrity. Znalezisko calculation zwykle wskazuje kod zaokrąglania nadawcy, codelist — dane główne, a profile — pole XRechnung, na przykład referencję nabywcy

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;  // noty pokrycia, jak 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;

Puszczaj tę samą pętlę także po schema.issues, bo błędy poprawności składniowej i elementu głównego lądują tam, a etap reguł biznesowych nigdy nie widzi dokumentu, którego nie dało się sparsować

Dlaczego ta sama faktura może przejść w CII i paść w UBL?

Oficjalne artefakty nie stosują tej samej tolerancji liczbowej w obu składniach, a PDFlibPas przepisuje każdą regułę z XSLT własnej składni zamiast je ujednolicać. BR-CO-17 to najjaśniejszy przykład. Sprawdza, czy kwota podatku kategorii VAT równa się bazie razy stawka po zaokrągleniu do dwóch miejsc, i obie składnie pozwalają na różnicę jednej jednostki waluty. Artefakt CII zapisuje to jako <= i >=; artefakt UBL używa abs z ostrym <

Weź bazę 100.00 przy 19 procentach, więc oczekiwany podatek to 19.00, i fakturę deklarującą 20.00. W CII 20.00 minus 1 równa się 19.00, co spełnia <=, i BR-CO-17 przechodzi. W UBL to samo porównanie jest ostre, 19.00 nie jest mniejsze od 19.00, i BR-CO-17 pada. Nikt nie wysyła takiej faktury celowo, ale generatory z błędem zaokrąglania siedzą dokładnie na takich granicach, a pakiet testów pokrywający tylko jedną składnię tego nie zauważy

Porównanie tolerancji BR-CO-17 w walidacji faktur PDFlibPas dla Delphi: ta sama faktura z różnicą zaokrąglenia jednej jednostki przechodzi w CII i pada w UBL
jedna jednostka zaokrąglenia siedzi dokładnie na granicy BR-CO-17, więc ta sama faktura przechodzi w CII i pada w UBL

Różnice idą dalej niż BR-CO-17. BR-xx-08 wymaga idealnej równości po zaokrągleniu do dwóch miejsc w CII, podczas gdy UBL pozwala na ±1 i reaguje tylko, gdy kategoria faktycznie ma pozycje, upusty albo opłaty. BR-O-08 jest ścisły w obu. Artefakt CII definiuje BR-AF-09 i BR-AG-09 jako true(), więc tam nigdy nie padają, podczas gdy UBL sprawdza je z ±1. A kiedy rozbicie VAT w CII w ogóle nie ma stawki, reguły kategorii spoza O przechodzą próżniowo, bo XPath every $rate po pustej sekwencji jest prawdą. Kod natywny musi je pomijać tak samo, inaczej zgłasza błędy na oficjalnych plikach przykładowych

Druga pułapka dotyczy identyfikatorów reguł. Prefiks kategorii reguły nie jest kodem kategorii VAT: K mapuje się na BR-IC, L na BR-AF, a M na BR-AG. Wyprowadzanie ID z kodu kategorii daje BR-K-09 i BR-K-10, identyfikatory, które w EN 16931 nie istnieją. Jeśli twoja aplikacja mapuje ID reguł na teksty pomocy, bierz ID z raportu i nigdy nie buduj go z kodu kategorii podatku

Jak włącza się reguły XRechnung i Peppol?

Wbudowany walidator dobiera reguły profilu z identyfikatora specyfikacji (BT-24). Identyfikator zawierający xrechnung uruchamia reguły CIUS XRechnung 3.0.2 na CII lub UBL; identyfikator rozszerzenia urn:xeinkauf.de:kosit:extension:xrechnung_3.0 dodatkowo włącza reguły BR-DEX. Kontrole kwot pozycji podfaktury i VAT (BR-DEX-02, -03 i -09 do -14) działają tylko na UBL; na CII BR-DEX-15 raportuje, że pozycje podfaktury są niedostępne. Identyfikator zawierający peppol uruchamia reguły Peppol BIS Billing 3.0.20, które Peppol definiuje wyłącznie dla UBL. Dokument CII podający się za customizację Peppol dostaje informacyjną notę BuiltIn.Coverage.Profile, że te reguły nie były sprawdzane, zamiast cichego przejścia. Dla innych profili CIUS użyj oficjalnych artefaktów

Szczegół UBL warto znać: BR-06 i BR-07 akceptują wyłącznie PartyLegalEntity/RegistrationName jako nazwę sprzedawcy lub nabywcy. Spaść na PartyName/Name wygląda pobłażliwie, ale kryje realne błędy BR-06, stąd ta kontrola jest ostra

Kiedy wciąż potrzebujesz oficjalnego Schematronu?

Używaj oficjalnych artefaktów, Schematronu EN 16931 przez Saxon albo walidatora KoSIT dla XRechnung, ilekroć wynik niesie wagę prawną albo kontraktową: odrzucenie faktury dostawcy, certyfikacja własnego generatora, rozstrzyganie sporu. Wbudowany raport nigdy nie ustawia fullConformance na true. Ta flaga wymaga zweryfikowanego pakietu reguł, obu zaliczonych etapów, obu etapów prowadzonych przez silnik wykonujący reguły własne pakietu i niczego w notCovered

ValidateInvoiceBusinessRulesWithPackage prowadzi tę ścieżkę. Pakiet reguł to katalog z manifestem rulepackage.xml pisanym przez CreateInvoiceRulePackage, który nazywa standard, wersję, schemę i arkusz Schematron oraz zapisuje SHA-256 każdego pliku. PDFlibPas sprawdza każdy skrót przy ładowaniu pakietu i ponownie tuż przed tym, jak silnik czyta plik, co zawęża okno na podmianę po weryfikacji. Ścieżki nie mogą wychodzić poza katalog pakietu, a natywny loader XSD odmawia importów, których manifest nie wymienia. Przy skonfigurowanym xmllint importowane schemy czyta sam xmllint, więc chroni je tylko kontrola z czasu ładowania

const
  // SHA-256 rulepackage.xml, które przejrzałeś i zaakceptowałeś
  ApprovedManifestSHA256 = '<64 hex digits from your release review>';
var
  Report, Pkg: TJSONObject;
begin
  // PDF: TPDFlib z już wczytaną fakturą, jak w pierwszym przykładzie
  Report := TJSONObject.ParseJSONValue(
    PDF.ValidateInvoiceBusinessRulesWithPackage('',
      'C:\Rules\EN16931-CII-1.3.16', ApprovedManifestSHA256,
      '',                          // pusty: natywny etap XSD
      'C:\Java\bin\java.exe', 'C:\Saxon\saxon-he.jar',
      0)) as TJSONObject;          // 0: 30000 ms na narzędzie
  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;

Przypięty skrót to nie opcjonalna dekoracja. Skróty plików dowodzą, że pliki pasują do manifestu, ale nic wewnątrz pakietu nie dowodzi, że sam manifest jest autentyczny: atakujący, który może przepisać reguły, może przepisać wraz z nimi i manifest. Tylko skrót ustalony przez wywołującego, w kodzie albo w podpisanej konfiguracji, zamyka tę lukę, a raport trzyma pinned i verified osobno, żebyś umiał rozpoznać, którą gwarancję faktycznie masz

Przepływ integralności pakietu reguł faktur PDFlibPas w Delphi: skróty SHA-256 manifestu sprawdzane przy ładowaniu i przed każdym odczytem, przypięte przez wywołującego
skróty plików chronią zawartość pakietu, a tylko skrót przypięty przez wywołującego chroni sam manifest

Jeśli przekażesz pusty JavaExecutable, etap schema nadal waliduje natywnie względem XSD pakietu, łącznie z fasadami i ograniczeniami tożsamości od v3.672.0, ale reguły biznesowe spadają na wbudowany podzbiór i fullConformance zostaje false. Konstrukcje schemy, których loader natywny nie wspiera — jak grupy podstawiania — wywalają ładowanie z XSD.Unsupported zamiast być pomijane

Gdzie pasują veraPDF i Mustang

Żadna z tych funkcji nie patrzy na kontener PDF. Zgodność z PDF/A-3 pod ISO 19005-3, rozszerzenie XMP Factur-X i relacja associated-file to osobne pytania, i veraPDF oraz Mustang pozostają dla nich narzędziami odniesienia, jak opisano w walidacji e-faktur z veraPDF i Mustang. Strona produkująca, od dołączania XML po deklarowanie profilu, jest opisana w budowaniu hybrydowych faktur Factur-X i ZUGFeRD w Delphi oraz schemach rozszerzeń PDF/A-3 dla metadanych Factur-X

Rozsądny podział uruchamia wbudowany test w procesie na każdej przychodzącej fakturze, gdzie kosztuje milisekundy: faktura UBL o rozmiarze ~1 MB z załącznikami base64 waliduje się w 15 ms, albo 79 ms z XSD UBL 2.1. Uruchomienie Saxon z pakietu idzie do CI i na ścieżkę odrzucenia, a kontener pokrywa veraPDF. Cokolwiek mówi wynik wbudowany, opisuje on pokryty podzbiór, nie standard — dokładnie po to pole coverage tam jest, by ci o tym przypomnieć

Funkcje walidacji faktur, katalog reguł i narzędzia pakietów reguł są w bibliotece PDF dla Delphi dla Delphi i C++Builder, razem z używanymi wyżej API Factur-X, PDF/A-3 i plików osadzonych