Artykuł techniczny

Hybrydowe faktury Factur-X i ZUGFeRD w Delphi

Zgodna faktura elektroniczna to nie PDF z dołączonym z boku plikiem XML. To pojedynczy dokument PDF/A-3, który zawiera fakturę dwa razy: raz jako stronę czytelną dla człowieka i raz jako czytelny maszynowo XML Cross Industry Invoice przechowywany wewnątrz pliku jako plik powiązany. Obie reprezentacje opisują tę samą fakturę. Ta podwójna natura jest istotą rodzin formatów, których wymagają teraz europejskie przepisy - Factur-X we Francji i Niemczech, ZUGFeRD na rynkach niemieckojęzycznych i XRechnung do fakturowania w sektorze publicznym w Niemczech. Ten artykuł pokazuje, jak PDFlibPas składa taką hybrydową fakturę w Delphi, gdzie standardy pozostawiają miejsce na błędy, i dlaczego jeden profil w katalogu wymaga całkowicie osobnego konstruktora XML

Czym właściwie jest faktura hybrydowa

Widoczna strona i osadzony XML obsługują różnych czytelników. Pracownik zatwierdzający płatność patrzy na wyrenderowaną stronę. System rozrachunków wczytuje XML, odczytuje sumy i podział podatkowy jako pola strukturalne i księguje wpis bez żadnego ręcznego wprowadzania danych. Treść semantyczna tego XML jest regulowana przez EN 16931 - europejski standard definiujący model danych faktury: które pola istnieją, co oznaczają i które są obowiązkowe. EN 16931 to model semantyczny, nie format pliku. Factur-X, ZUGFeRD 2.x i XRechnung realizują ten model jako dokument UN/CEFACT Cross Industry Invoice - składnię przenoszącą pola EN 16931 przez sieć

Aby dokument był zarówno archiwalny, jak i samoopisujący, kontenerem jest PDF/A-3 zdefiniowany przez ISO 19005-3. PDF/A-3 to poziom zgodności, który dopuszcza dowolne osadzone pliki - dokładnie tego potrzebuje XML faktury. PDF/A-2 zabrania osadzania plików, które same nie są w formacie PDF/A, więc faktura Factur-X nie może być w formacie PDF/A-2. Wybór PDF/A-3 to zatem nie preferencja, lecz wymóg wynikający bezpośrednio z potrzeby osadzenia danych nie będących PDF w dokumencie archiwalnym

Dlaczego relacja to Alternative

Osadzanie bajtów jest łatwą częścią. ISO 32000 §7.11.4 definiuje strumień osadzonego pliku - obiekt przechowujący surowy XML i jego parametry. Częścią, która czyni plik prawidłowym plikiem powiązanym, jest §14.13, który dodaje pojęcie pliku powiązanego i klucz /AFRelationship. Klucz ten określa, jak osadzone dane odnoszą się do treści, do której są dołączone, a wartością mandatowaną przez Factur-X jest Alternative

Wybór ma znaczenie, ponieważ inne wartości twierdziłyby coś fałszywego o dokumencie. Source oznaczałoby, że XML jest materiałem, z którego wygenerowano widoczną treść - wzorzec, od którego pochodzi strona. Supplement oznaczałoby, że XML dodaje informacje poza tym, co pokazuje strona - coś ekstra nie zawartego w renderingu. Żadne z nich nie opisuje faktury Factur-X. XML i strona to dwa równoważne wyrazy jednej faktury, niosące tę samą treść prawną w dwóch formach. Alternative to wartość mówiąca dokładnie to: równoważna alternatywna reprezentacja widocznej treści. Walidator, który odczytuje jakąkolwiek inną relację w pliku Factur-X, odrzuci go - i słusznie, bo relacja jest maszynowo czytelnym twierdzeniem o tym, do czego służy załącznik

Katalog profili

Przykład E-Invoice dostarczany z PDFlibPas uruchamia tę samą ścieżkę generowania dla sześciu profili zdefiniowanych jako tablica rekordów w InvoiceModel.pas. Każdy profil zawiera wartości potrzebne generatorowi: nazwę wyświetlaną, nazwę osadzonego pliku, poziom zgodności, /AFRelationship, wersję, opcjonalny kod kraju i URN GuidelineID, który XML ogłasza w kontekście dokumentu

Sześć profili to: Factur-X EN16931, Factur-X BASIC, Factur-X EXTENDED dla Francji, XRechnung 3.0, ZUGFeRD 1.0 COMFORT i ZUGFeRD 2.0 BASIC. GuidelineID to pole, które dokładnie informuje odbiorcę, jakiego profilu się spodziewać, a wartości są specyficzne. Factur-X EN16931 ogłasza urn:cen.eu:en16931:2017. XRechnung 3.0 ogłasza urn:cen.eu:en16931:2017#compliant#urn:xeinkauf.de:kosit:xrechnung_3.0. ZUGFeRD 2.0 BASIC ogłasza urn:cen.eu:en16931:2017#compliant#urn:zugferd.de:2p0:basic. Nazwa osadzonego pliku jest też częścią kontraktu. Profile Factur-X osadzają factur-x.xml, XRechnung osadza xrechnung.xml, a profile ZUGFeRD osadzają ZUGFeRD-invoice.xml lub zugferd-invoice.xml. Odbiorca skanuje nazwy załączników, aby znaleźć fakturę, więc nazwa pliku nie jest kosmetyczna

Jeden szczegół w katalogu wart uważnego przeczytania: większość profili używa relacji Alternative, ale wpis XRechnung 3.0 w przykładzie używa Source. Dwa formaty podlegają różnym walidatorom i konwencjom, a przykład ustawia relację każdego profilu z katalogu zamiast zakodować jedną stałą wartość - dlatego właśnie istnieje pole per-profil, a nie stała

Pułapka ZUGFeRD 1.0

Kusi, by założyć, że każdy profil to EN 16931 Cross Industry Invoice z drobnymi różnicami w liczbie wypełnianych pól opcjonalnych. To prawda dla pięciu z sześciu. Nie dotyczy ZUGFeRD 1.0 COMFORT, a przyczyna jest strukturalna, nie kosmetyczna

Nowoczesne profile emitują UN/CEFACT Cross Industry Invoice z wersją przestrzeni nazw :100, którego elementem głównym jest rsm:CrossIndustryInvoice. ZUGFeRD 1.0 poprzedza ten schemat. Jest to CrossIndustryDocument z 2014 roku z wersją przestrzeni nazw :1p0, a jego elementem głównym jest rsm:CrossIndustryDocument. URN przestrzeni nazw są różne, element główny jest inny, a drzewo elementów różni się w całości: schemat :1p0 grupuje dane pod ApplicableSupplyChainTradeAgreement, ApplicableSupplyChainTradeDelivery i ApplicableSupplyChainTradeSettlement, podczas gdy :100 używa ApplicableHeaderTradeAgreement, ApplicableHeaderTradeDelivery i ApplicableHeaderTradeSettlement. Nazewnictwo jest wystarczająco podobne, by wprowadzić w błąd, i wystarczająco różne, by powodować błędy

Słowo COMFORT w nazwie profilu opisuje bogactwo danych - profil klasy automatyzacyjnej z pełnymi pozycjami, podziałem podatkowym i warunkami płatności - a nie to, który schemat go przenosi. Nie można więc wziąć dokumentu :100 i oznaczyć go dla ZUGFeRD 1.0. Przykład obsługuje to flagą w każdym rekordzie profilu i dwoma oddzielnymi funkcjami konstruktora, wybierając właściwą przed wygenerowaniem jakiegokolwiek XML

function BuildInvoiceXMLText(const AProfile: TeInvoiceProfile;
  const Data: TInvoiceData): string;
begin
  // XMLFamily = 1 means the legacy ZUGFeRD 1.0 :1p0 schema; every
  // other profile is the modern UN/CEFACT :100 Cross Industry Invoice.
  if AProfile.XMLFamily = 1 then
    Result := BuildZUGFeRD1Text(AProfile, Data)
  else
    Result := BuildCII100Text(AProfile, Data);
end;

Podział nie jest uprzejmością implementacyjną. Przekazanie drzewa :100 do odbiorcy ZUGFeRD 1.0 powoduje, że dokument nie przechodzi walidacji schematu na poziomie elementu głównego, więc obie rodziny muszą być budowane przez kod, który wie, którą z nich pisze

Wybór poziomu PDF/A-3

PDF/A-3 ma trzy poziomy zgodności, a PDFlibPas wybiera je przez SetPDFAMode. Tryb 5 to PDF/A-3b - poziom gwarantujący wierną reprodukcję wizualną. Tryb 6 to PDF/A-3a, który dodaje wymagania tagowanej struktury i dostępności poziomu a. Tryb 7 to PDF/A-3u, który wymaga, aby cały tekst był odwzorowany na Unicode. Włączenie trybu osadza też wbudowany profil kolorów sRGB biblioteki - charakterystykę kolorystyczną, której PDF/A wymaga, aby renderowany kolor był zdefiniowany, a nie zależny od urządzenia

Większość przepływów faktur działa na poziomie 3b, co jest wystarczające dla wiernej strony widocznej i osadzonego XML. Jeśli potrzebujesz jawnego profilu ICC zamiast wbudowanego, LoadOutputIntentProfile zamienia go po ustawieniu trybu. Przykład ładuje w ten sposób profil sRGB repozytorium i wraca do wbudowanego zamiaru, gdy plik nie jest dostępny, więc output intent jest zawsze obecny

PDF := TPDFlib.Create;
try
  // Mode 5 = PDF/A-3b, 6 = PDF/A-3a, 7 = PDF/A-3u.
  if PDF.SetPDFAMode(5) <> 1 then
    raise Exception.Create('PDF/A-3 mode could not be enabled');

  // Optional: swap the built-in sRGB intent for an explicit ICC profile.
  if PDF.LoadOutputIntentProfile(ICCFile, 'DeviceRGB') <> 1 then
    { fall back to the built-in sRGB intent that SetPDFAMode embedded };
finally
  // ... continue building the document
end;

Budowanie faktury hybrydowej

Po skonfigurowaniu kontenera pozostają trzy kroki w kolejności: ustawić tryb PDF/A-3, narysować stronę czytelną dla człowieka, a następnie dołączyć XML jako plik powiązany. Strona widoczna to zwykła treść. Jedno ograniczenie warte zapamiętania: PDF/A zabrania nieosadzonych standardowych 14 czcionek, więc strona musi osadzać prawdziwy krój czcionki zamiast odwoływać się do wbudowanego

Załącznik to jedno wywołanie. AddFacturXAssociatedFileFromString przyjmuje surowe bajty XML UTF-8 oraz metadane profilu, zapisuje strumień osadzonego pliku, rejestruje go w tablicy /AF Katalogu wymaganej przez PDF/A-3, stosuje /AFRelationship i generuje metadane XMP faktury elektronicznej identyfikujące dokument jako Factur-X, ZUGFeRD lub XRechnung. Sprawdza też, czy GuidelineID w XML zgadza się z zadeklarowanym poziomem zgodności, więc niezgodność między zbudowanym XML a nazwanym profilem jest wychwycona, a nie po cichu wysłana

// 1. PDF/A-3 mode and output intent are already set.
// 2. Draw the visible page (embeds a real TrueType font).
DrawInvoicePage(PDF, AProfile, Data);

// 3. Build the profile-correct XML and attach it as an
//    associated file with /AFRelationship = Alternative.
InvoiceXML := BuildInvoiceXML(AProfile, Data);   // AnsiString of UTF-8 bytes
FileID := PDF.AddFacturXAssociatedFileFromString(
  InvoiceXML,
  AProfile.ConformanceLevel,   // e.g. 'EN16931'
  AProfile.FileName,           // 'factur-x.xml'
  AProfile.Description,
  AProfile.Relationship,       // 'Alternative'
  AProfile.Version,            // '1.0'
  AProfile.CountryCode);       // '' or 'DE' or 'FR'
if FileID <= 0 then
  raise Exception.Create('Invoice XML could not be attached');

PDF.SaveToFile(TargetFile);

Jedną subtelną kwestią w ścieżce danych jest kodowanie. Osadzony XML deklaruje encoding="UTF-8", a metoda przyjmuje bajty jako AnsiString, więc nieasciinowa nazwa sprzedawcy lub nabywcy musi dotrzeć do wywołania jako surowe oktety UTF-8. Zwykłe rzutowanie przez stronę kodową systemu ANSI skorumpowałoby te znaki i po cichu wyprodukowałoby fakturę, której XML nie odpowiada już własnej deklaracji. Przykład koduje do UTF-8 jawnie przed przekazaniem bajtów, co jest bezpiecznym sposobem podawania bajtów dowolnemu API PDF zorientowanemu na bajty z Unicode string

Aby dołączyć XML, który nie jest rozpoznanym profilem faktury elektronicznej, AddPDFA3AssociatedFileFromString jest ogólnym odpowiednikiem. Przyjmuje nazwę pliku, typ MIME, opis, relację i bajty, i zapisuje zwykły plik powiązany PDF/A-3 bez żadnych metadanych specyficznych dla faktur ani sprawdzania wytycznych. Używaj go do danych uzupełniających; używaj metody Factur-X dla faktur, aby metadane profilu i dopasowanie wytycznej były zapisywane za ciebie

Po wyprodukowaniu dokumentu następne pytania dotyczą tego, czy przechodzi walidację PDF/A i dostępności, i czy może być podpisany bez naruszania zgodności. Są one omówione w przewodniku inspekcji wstępnej PDF/A i PDF/UA oraz warsztacie zgodności i podpisywania. Wszystko to jest dostarczane jako część PDFlibPas Delphi PDF Library, wraz z API PDF/A, tagowania i właściwości dokumentu, na których opiera się ścieżka faktur elektronicznych