Technical Article

Validace elektronických faktur: veraPDF a Mustang v Delphi

Faktura Factur-X nebo ZUGFeRD jsou vlastně dva dokumenty pod jedním názvem souboru. Vnější dokument je kontejner PDF/A-3, který musí archivační čtečky akceptovat dalších deset let. Vnitřní dokument je faktura ve formátu XML, kterou musí účetní systém kupujícího zpracovat podle normy EN 16931. Chyba, kvůli které se do produkce dostanou rozbité faktury, spočívá v přesvědčení, že pokud je správně ten první, ten druhý získáme zdarma. Není tomu tak. Soubor může být bezchybným PDF/A-3 a přitom obsahovat XML, které žádný daňový úřad nepřijme, nebo může obsahovat ukázkové XML podle EN 16931 uvnitř kontejneru, který neprojde archivační validací. Obě vrstvy validují dva různé nástroje, které o sobě navzájem nic nevědí, a reálná pipeline musí vyhovět oběma

Dva validátory, dvě různé otázky

Nástroj veraPDF je referenční implementace pro PDF/A. Pokud jej nasměrujete na fakturu, odpoví na jedinou otázku: je to vyhovující soubor PDF/A-3. Kontroluje věci, které zajímají normu ISO 19005-3. Je každé písmo vloženo. Existuje OutputIntent. Deklarují XMP metadata správnou část a úroveň shody. U elektronické faktury také kontroluje strukturu pro přidružené soubory, kterou vyžaduje PDF/A-3, protože XML se veze s sebou jako vložený soubor s relací /AFRelationship a záznamem v poli /AF katalogu dokumentu. O tom, zda celková částka faktury sedí, nástroj veraPDF neřekne nic, protože to nespadá do jeho kompetence

Mustang je open-source validátor z projektu Mustangproject. Klade si ortogonální otázku: je vložené XML platnou fakturou. Porovnává XML se schématem deklarovaného profilu a následně na něj aplikuje obchodní pravidla EN 16931 a na ně navrstvené sady pravidel pro konkrétní zemi, včetně CIUS pro XRechnung. Kontroluje, zda je přítomno identifikační číslo DPH prodejce, pokud si to součty vyžadují, zda se částky slev a přirážek shodují s celkovou částkou dokladu, a zda URN profilu v XML odpovídá tomu, za co se soubor vydává. Nástroj Mustang se nestará o to, zda okolní PDF obsahuje svá písma, protože to je úkolem nástroje veraPDF

Žádný z obou nástrojů není nadmnožinou toho druhého. Nástroj veraPDF propustí strukturálně dokonalý kontejner obalující nesmyslné XML. Mustang propustí dokonalé XML zabalené v kontejneru s chybějícím OutputIntent. Každý zachytí přesně tu třídu vad, vůči které je ten druhý slepý, a to je přesně ten důvod, proč seriózní validační sada spouští oba a považuje soubor za připravený k odeslání, až když se oba shodnou

Validační matice

Abychom prokázali, že knihovna produkuje soubory, které přežijí obě brány, staví testovací sada matici. Šest profilů faktur pokrývá rozsah, se kterým se evropská pipeline setkává v praxi: Factur-X EN 16931, Factur-X BASIC, varianta Factur-X EXTENDED France B2B, XRechnung 3.0, ZUGFeRD 1.0 COMFORT a ZUGFeRD 2.0 BASIC. Každý profil se generuje pro dvě dílčí úrovně shody PDF/A, 3b a 3u, protože požadavky na úroveň B a úroveň U se rozcházejí v mapování znaků Unicode a soubor, který projde jednou, může u druhé selhat. Šest profilů krát dvě úrovně představuje dvanáct souborů, z nichž každý je sestaven v bezhlavém (headless) režimu pomocí stejné cesty v kódu, jakou obsahuje ukázka GUI, takže testované artefakty nejsou pro test nijak ručně doladěny

Generátor zapíše všech dvanáct a skript vloží každý z nich do obou validátorů. Při prvním plném běhu prošlo nástrojem veraPDF všech dvanáct. Struktura kontejneru byla ve všech případech správná: přidružené soubory byly zaregistrovány, shoda s XMP deklarována a output intenty na svém místě. Nástrojem Mustang jich prošlo osm. Čtyři faktury byly strukturálně platné soubory PDF/A-3 obsahující XML, které validátor obchodních pravidel odmítl, což je přesně ten typ rozdílu, který má přístup dvou nástrojů odhalit. Kdyby testovací sada důvěřovala pouze nástroji veraPDF, vypadaly by tyto čtyři jako hotové

Dvě opravy, které tento rozdíl smazaly

Čtyři selhání nástroje Mustang pramenila ze dvou různých příčin a oprava pro každou z nich je detailem, který stojí za to znát předtím, než si tyto profily začnete sami generovat

První byl profil Factur-X EXTENDED France B2B. Původní generátor předal interní štítek jako úroveň shody a interní URN jako guideline (pokyn), a nástroj Mustang soubor odmítl s chybou o neplatné hodnotě shody následovanou chybou o nepodporovaném typu profilu. Důvodem je, že pole XMP fx:ConformanceLevel není volně textové místo pro pojmenování vlastního profilu. Standard Factur-X pro něj definuje přesně pět standardních hodnot: MINIMUM, BASIC WL, BASIC, EN 16931 a EXTENDED. Faktura B2B specifická pro Francii je z pohledu metadat XMP stále dokumentem profilu EXTENDED. Francouzský charakter faktury se nevyjadřuje tím, že si vymyslíme šestou hodnotu shody. Vyjadřuje se kódem země, FR, a identifikátorem guideline uvnitř XML, který musí nést předponu urn:cen.eu:en16931:2017#conformant#, označující CIUS v souladu s EN 16931. Předáním standardní hodnoty EXTENDED s FR jako kódem země a správného URN guideline se soubor stal vyhovujícím

V API knihovny to představuje volání metody AddFacturXAssociatedFileFromString se sladěnou úrovní shody, zemí a identifikátorem guideline. Argument pro úroveň shody nese standardní token, argument pro kód země nese FR a URN guideline sídlí v bajtech XML, které jí předáváte

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;

Druhou příčinou byl profil ZUGFeRD 1.0 COMFORT a neměl nic společného s metadaty. ZUGFeRD 1.0 je validován proti XSD :1p0, které je ohledně kardinality přísnější, než jak to naznačují psaná shrnutí. XSD vyžaduje, aby souhrnná částka zúčtování v hlavičce, ram:SpecifiedTradeSettlementMonetarySummation, obsahovala ram:ChargeTotalAmount a ram:AllowanceTotalAmount, každou právě jednou. Vygenerované XML oba prvky vynechalo, takže Mustang nahlásil, že se tyto elementy musí vyskytnout právě jednou. Tyto prvky nejsou volitelné, pokud schéma říká, že minOccurs je jedna. Zápis obou v pořadí odpovídajícím XSD, bezprostředně po ram:LineTotalAmount, s hodnotou 0.00 v případě, že neexistují žádné poplatky nebo slevy, splnil požadavky schématu. Nula znamená přítomný element; nepřítomný element znamená porušení schématu. Po zavedení těchto dvou oprav se matice dostala na dvanáct z dvanácti pro Mustang, zatímco u veraPDF zůstala na dvanácti z dvanácti

Pole v XRechnung, která překlopí neplatné na platné

Formát XRechnung si zaslouží zvláštní zmínku, protože jeho německý CIUS přidává obchodní pravidla, která v základní sadě EN 16931 chybí, a ta selhávají způsoby, u nichž to na první pohled vypadá, že s dokumentem není nic v nepořádku. Dvě z nich se týkají elektronických adres. BT-34 je elektronická adresa prodejce a BT-49 je elektronická adresa kupujícího, což jsou koncové body směrování, které portál veřejného sektoru v Německu používá pro doručování a potvrzování faktury. Základní model EN 16931 je považuje za volitelné. XRechnung nikoliv. Vynechání kteréhokoli z nich znamená, že je faktura sice formálně správná a platná podle schématu, ale bude odmítnuta

Třetím je pravidlo BR-DE-6, které vyžaduje přítomnost kontaktního telefonního čísla na prodejce. Jde o přesně ten druh pole, které vývojář vynechá, protože to vnímá spíše jako prezentaci než jako data, a jeho absence vyvolá chybu ve validaci, jež ukazuje spíše na skupinu kontaktů na prodejce než na cokoli zjevně chybějícího. Doplnění hodnot BT-34, BT-49 a telefonního čísla prodejce je tím, co v rámci nástroje Mustang překlopí soubor XRechnung z neplatného na platný, a nic z toho nezmění nic z toho, co vidí veraPDF, protože všechny tři hodnoty sídlí uvnitř XML

Propojení výstupu z knihovny do validátoru

Architektonický smysl celé této sady lze zobecnit na jakýkoliv podnikový systém. Knihovna pro práci s PDF zapíše kontejner, který vyhovuje požadavkům, a vloží do něj XML. Nepokouší se a ani by se neměla pokoušet být autoritou pro obchodní pravidla podle normy EN 16931. Metoda ValidateFacturXInvoice v knihovně kontroluje konzistenci kontejneru, tedy že se shodují pole /AF v katalogu, strom názvů vložených souborů, DocumentFileName v XMP metadatech, profil, guideline a relace /AFRelationship, ale neprovádí validaci daňových kódů ani nenarovnává částky. Správná dělba práce spočívá v tom, že podnikový systém extrahuje XML a předá jej dedikovanému validátoru faktur, přesně tak, jako jej testovací sada předává nástroji Mustang

Zpětným přečtením souboru zjistíte, co se do něj skutečně zapsalo. Metoda DetectFacturXInvoice oznámí, zda byla faktura rozpoznána, a GetFacturXInvoiceInfo přečte pole metadat podle tagu: tag 1 je název vloženého souboru, tag 2 je DocumentFileName v XMP, tag 5 je úroveň shody, tag 6 je identifikátor guideline a tag 7 je relace /AFRelationship. Potvrzení, že načtená úroveň shody je standardním tokenem, a nikoli interním označením, je ten nejlevnější způsob, jak zachytit chybu ohledně úrovně EXTENDED předtím, než soubor opustí váš build proces

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;

Metoda ExtractFacturXXMLToString vrací surové bajty XML jako typ AnsiString, připravené k zápisu do souboru nebo ke streamování do procesu validátoru. V našem testovacím prostředí je tímto cílem nástroj Mustang, který je vyvolán přes příkazovou řádku balíčku jar, a při stejném průchodu nad stejným souborem se spouští i nástroj veraPDF. Celé toto propojení je malé: konzolový generátor, EInvoiceValidation.dpr, zapisuje oněch dvanáct souborů pomocí sdíleného modelu faktury z ukázky a skript run-validation.ps1 pohání oba validátory napříč výstupním adresářem a vypisuje tabulku úspěchů a selhání. Tento dvoufázový postup (vygenerovat s knihovnou a ověřit pomocí externích validátorů) je přesně to, co by měla provádět úloha kontinuální integrace při každé změně generování faktur, protože jediný způsob, jak zjistit, zda soubor splňuje obě vrstvy, je zeptat se obou nástrojů

Pokud vaše pipeline vyžaduje také certifikaci kontejneru před jeho podpisem, problematika „preflight“ kontroly je popsána v našem průvodci k PDF/A a PDF/UA preflight kontrolám v Delphi a širší proces certifikace a následného podpisu je popsán v článku o pracovním prostředí pro soulad s předpisy a elektronické podepisování. Oba postupy staví na stejném generovacím mechanismu, který je dodáván jako součást produktu Delphi PDF Library pro Delphi a C++Builder společně s rozhraními API pro PDF/A, přidružené soubory a metadata, jež jsme zde využili