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