Artykuł techniczny

Walidacja faktur elektronicznych: veraPDF i Mustang w Delphi

Faktura Factur-X lub ZUGFeRD to dwa dokumenty ukryte pod jedną nazwą pliku. Dokument zewnętrzny to kontener PDF/A-3, który system archiwizacyjny musi zaakceptować przez następne dziesięć lat. Dokument wewnętrzny to faktura XML, którą system księgowy nabywcy musi przetworzyć zgodnie z EN 16931. Błąd prowadzący do trafiania wadliwych faktur na produkcję polega na przekonaniu, że poprawność pierwszej warstwy gwarantuje poprawność drugiej. Tak nie jest. Plik może być doskonałym dokumentem PDF/A-3, a mimo to zawierać XML, którego żaden urząd skarbowy nie zaakceptuje, i odwrotnie - może zawierać wzorcowy XML EN 16931 w kontenerze, który nie przejdzie walidacji archiwalnej. Obie warstwy weryfikowane są przez dwa różne narzędzia, które nic o sobie nie wiedzą, a prawdziwy potok przetwarzania musi spełniać wymagania obu

Dwa walidatory, dwa różne pytania

veraPDF jest referencyjną implementacją dla PDF/A. Wskaż mu fakturę, a odpowie na jedno pytanie: czy to jest zgodny plik PDF/A-3. Sprawdza to, co jest istotne dla ISO 19005-3: czy wszystkie czcionki są osadzone, czy istnieje OutputIntent, czy metadane XMP deklarują właściwą część i poziom zgodności. W przypadku faktury elektronicznej sprawdza też okablowanie pliku powiązanego wymaganego przez PDF/A-3, ponieważ XML jest dołączany jako osadzony plik z /AFRelationship i wpisem w tablicy /AF katalogu dokumentu. veraPDF nie wypowiada się w kwestii tego, czy suma faktury się zgadza, bo to wykracza poza jego zakres

Mustang to walidator open-source projektu Mustangproject. Zadaje pytanie ortogonalne: czy osadzony XML jest prawidłową fakturą. Sprawdza XML względem schematu dla zadeklarowanego profilu, a następnie stosuje reguły biznesowe EN 16931 i zestawy reguł specyficzne dla danego kraju, w tym CIUS XRechnung. Weryfikuje, czy identyfikator VAT sprzedawcy jest obecny, gdy wymagają tego kwoty, czy wartości upustów i dopłat bilansują się z sumą dokumentu, czy URN profilu w XML zgadza się z tym, czym plik twierdzi, że jest. Mustang nie dba o to, czy otaczający PDF osadza swoje czcionki, bo to jest zadanie veraPDF

Żadne z tych narzędzi nie jest nadzbiorem drugiego. veraPDF przepuszcza strukturalnie doskonały kontener z bezsensownym XML. Mustang przepuszcza doskonały XML opakowany w kontener z brakującym OutputIntent. Każde wychwytuje dokładnie tę klasę defektów, na którą drugie jest ślepe - i to właśnie jest cały powód, dla którego poważna uprząż walidacyjna uruchamia oba i traktuje plik jako gotowy do wysyłki dopiero wtedy, gdy oba się zgadzają

Macierz walidacji

Aby udowodnić, że biblioteka produkuje pliki przechodzące przez obie bramki, uprząż buduje macierz. Sześć profili faktur pokrywa zakres, z jakim spotyka się europejski potok przetwarzania: Factur-X EN 16931, Factur-X BASIC, wariant Factur-X EXTENDED dla Francji B2B, XRechnung 3.0, ZUGFeRD 1.0 COMFORT i ZUGFeRD 2.0 BASIC. Każdy profil generowany jest na dwóch podpoziomach zgodności PDF/A: 3b i 3u, ponieważ wymagania poziomu B i U różnią się w kwestii mapowania Unicode, a plik przechodzący przez jeden może nie przejść przez drugi. Sześć profili razy dwa poziomy to dwanaście plików - każdy z nich budowany bezgłowo tą samą ścieżką kodu, z której korzysta przykład GUI, więc testowane artefakty nie są ręcznie dostrojone pod kątem testu

Generator tworzy wszystkie dwanaście plików, a skrypt podaje każdy z nich obu walidatorom. W pierwszym pełnym uruchomieniu veraPDF zdał wszystkie dwanaście. Okablowanie kontenera było poprawne we wszystkich przypadkach: pliki powiązane zarejestrowane, zgodność XMP zadeklarowana, output intenty na miejscu. Mustang zdał osiem. Cztery faktury były strukturalnie prawidłowymi plikami PDF/A-3 zawierającymi XML, który walidator reguł biznesowych odrzucił - dokładnie to rozdzielenie, które podejście dwunarzędziowe ma ujawniać. Gdyby uprząż ufała tylko veraPDF, te cztery wyglądałyby na gotowe

Dwie poprawki, które wypełniły lukę

Cztery błędy Mustanga miały dwie odrębne przyczyny, a poprawka każdej z nich to szczegół wart poznania przed samodzielnym generowaniem tych profili

Pierwsza dotyczyła profilu Factur-X EXTENDED France B2B. Oryginalny generator przekazywał wewnętrzną etykietę jako poziom zgodności i wewnętrzny URN jako wytyczną, a Mustang odrzucił plik z błędem nieprawidłowej wartości zgodności, po którym nastąpił błąd nieobsługiwanego typu profilu. Przyczyną jest to, że pole XMP fx:ConformanceLevel nie jest wolnym polem tekstowym na własne nazwy profili. Factur-X definiuje dokładnie pięć standardowych wartości: MINIMUM, BASIC WL, BASIC, EN 16931 i EXTENDED. Faktura B2B specyficzna dla Francji jest nadal dokumentem w profilu EXTENDED z punktu widzenia metadanych XMP. Francuski charakter faktury nie jest wyrażany przez wymyślanie szóstej wartości zgodności. Wyraża się go kodem kraju, FR, oraz identyfikatorem wytycznej wewnątrz XML, który musi zawierać prefiks urn:cen.eu:en16931:2017#conformant# oznaczający CIUS zgodny z EN 16931. Przekazanie standardowej wartości EXTENDED z FR jako kodem kraju i prawidłowym URN wytycznej uczyniło plik zgodnym

W API biblioteki jest to wywołanie AddFacturXAssociatedFileFromString z wyrównaną zgodnością, krajem i wytyczną. Argument poziomu zgodności zawiera standardowy token, argument kodu kraju zawiera FR, a URN wytycznej żyje w bajtach XML, które przekazujesz

var
  FileID: Integer;
begin
  PDF.SetPDFAMode(5);            // PDF/A-3b
  PDF.NewDocument;
  // ... draw the human-readable invoice page ...
  // ExtendedXML carries an EN 16931 guideline URN of the form
  //   urn:cen.eu:en16931:2017#conformant#urn:factur-x.eu:1p0:extended
  FileID := PDF.AddFacturXAssociatedFileFromString(
    ExtendedXML,
    'EXTENDED',          // standard fx:ConformanceLevel, not an internal label
    'factur-x.xml',
    'Factur-X EXTENDED invoice',
    'Alternative',       // /AFRelationship
    '1.0',
    'FR');               // France B2B marked by country code, not by conformance
  if FileID = 0 then
    raise Exception.Create('Factur-X attachment rejected');
  PDF.SaveToFile('02_Factur-X-EXTENDED-FR_PDFA-3b.pdf');
end;

Druga przyczyna dotyczyła profilu ZUGFeRD 1.0 COMFORT i nie miała nic wspólnego z metadanymi. ZUGFeRD 1.0 jest walidowany względem XSD :1p0, który jest bardziej rygorystyczny w kwestii krotności niż sugerują opisowe podsumowania. XSD wymaga, aby nagłówkowa suma rozliczeniowa, ram:SpecifiedTradeSettlementMonetarySummation, zawierała ram:ChargeTotalAmount i ram:AllowanceTotalAmount dokładnie po jednym razie. Generowany XML pomijał oba, więc Mustang zgłosił, że elementy muszą wystąpić dokładnie jeden raz. Nie są one opcjonalne, gdy schemat mówi, że minOccurs wynosi jeden. Emitowanie obu w kolejności sekwencji XSD, bezpośrednio po ram:LineTotalAmount, z wartością 0.00 gdy nie ma opłat ani upustów, spełniło schemat. Zero to obecny element; brakujący element to naruszenie schematu. Po wprowadzeniu tych dwóch poprawek macierz osiągnęła wynik dwanaście na dwanaście w Mustangu, utrzymując dwanaście na dwanaście w veraPDF

Pola XRechnung, które zmieniają fakturę z nieprawidłowej na prawidłową

XRechnung zasługuje na osobną wzmiankę, ponieważ jego niemieckie CIUS dodaje reguły biznesowe nieobecne w podstawowym zestawie EN 16931, a naruszenie tych reguł wygląda tak, jakby z dokumentem nic nie było nie tak. Dwie z nich dotyczą adresów elektronicznych. BT-34 to elektroniczny adres sprzedawcy, a BT-49 to elektroniczny adres nabywcy - punkty routingu, które portal sektora publicznego w Niemczech wykorzystuje do dostarczania i potwierdzania faktur. Podstawowy model EN 16931 traktuje je jako opcjonalne. XRechnung nie. Pominięcie któregokolwiek powoduje, że faktura jest poprawnie sformułowana, zgodna ze schematem i odrzucona

Trzecia to reguła BR-DE-6, która wymaga obecności numeru telefonu kontaktowego sprzedawcy. To rodzaj pola, które programista pomija, bo wygląda bardziej jak prezentacja niż dane, a jego brak powoduje błąd walidacji wskazujący na grupę kontaktową sprzedawcy, a nie na cokolwiek oczywistego. Podanie BT-34, BT-49 i numeru telefonu sprzedawcy przesuwa plik XRechnung z nieprawidłowego do prawidłowego w Mustangu, a żadna z tych zmian nie wpływa na to, co widzi veraPDF, bo wszystkie trzy żyją w XML

Podłączanie wyjścia biblioteki do walidatora

Architektoniczny punkt stojący za uprzężą uogólnia się na każdy system biznesowy. Biblioteka PDF zapisuje zgodny kontener i osadza XML. Nie próbuje i nie powinna próbować być autorytetem reguł biznesowych EN 16931. ValidateFacturXInvoice w bibliotece sprawdza spójność kontenera - że tablica /AF katalogu, drzewo nazw osadzonych plików, DocumentFileName XMP, profil, wytyczna i /AFRelationship są ze sobą zgodne - ale nie waliduje kodów podatkowych ani nie bilansuje kwot. Właściwy podział pracy polega na tym, że system biznesowy wyodrębnia XML i przekazuje go do dedykowanego walidatora faktur, dokładnie tak jak uprząż przekazuje go do Mustanga

Odczytanie pliku z powrotem mówi ci, co zostało faktycznie zapisane. DetectFacturXInvoice informuje, czy faktura została rozpoznana, a GetFacturXInvoiceInfo odczytuje pola metadanych po tagu: tag 1 to nazwa osadzonego pliku, tag 2 to DocumentFileName XMP, tag 5 to poziom zgodności, tag 6 to identyfikator wytycznej, a tag 7 to /AFRelationship. Potwierdzenie, że odczytany poziom zgodności to standardowy token, a nie wewnętrzna etykieta, to najtańszy sposób wychwycenia błędu EXTENDED przed opuszczeniem pliku przez twój build

function ExtractAndInspect(const PdfPath: string): AnsiString;
var
  Profile, Guideline: WideString;
begin
  Result := '';
  PDF.LoadFromFile(PdfPath);
  if PDF.DetectFacturXInvoice = 1 then
  begin
    Profile   := PDF.GetFacturXInvoiceInfo(5);  // fx:ConformanceLevel
    Guideline := PDF.GetFacturXInvoiceInfo(6);  // XML guideline ID
    Writeln('Profile:   ', Profile);
    Writeln('Guideline: ', Guideline);
    // Hand the raw XML to a dedicated EN 16931 / Mustang validator.
    Result := PDF.ExtractFacturXXMLToString;
  end;
end;

ExtractFacturXXMLToString zwraca surowe bajty XML jako AnsiString, gotowe do zapisania do pliku lub przesłania strumieniowo do procesu walidatora. W uprzęży testowej tym celem jest Mustang, wywoływany przez jego jar wiersza poleceń, a veraPDF uruchamiany w tym samym przebiegu nad tym samym plikiem. Okablowanie jest małe: generator konsolowy, EInvoiceValidation.dpr, tworzy dwanaście plików przy użyciu współdzielonego modelu faktury z przykładu, a skrypt, run-validation.ps1, uruchamia oba walidatory nad katalogiem wyjściowym i drukuje tabelę zaliczeń i niepowodzeń. Ten sam dwuetapowy schemat - generuj z biblioteką i weryfikuj zewnętrznymi walidatorami - powinien być uruchamiany przez zadanie ciągłej integracji przy każdej zmianie w generowaniu faktur, ponieważ jedynym sposobem, aby wiedzieć, że plik spełnia obie warstwy, jest zapytanie obu narzędzi

Jeśli twój potok musi też certyfikować kontener przed podpisaniem, strona wstępna tego procesu jest omówiona w naszym przewodniku po inspekcji wstępnej PDF/A i PDF/UA w Delphi, a szerszy przepływ certyfikuj-a-następnie-podpisz jest opisany w warsztacie zgodności i podpisywania. Oba opierają się na tej samej ścieżce generowania, która jest dostarczana jako część Delphi PDF Library dla Delphi i C++Builder, wraz z API PDF/A, plików powiązanych i metadanych używanymi tutaj