Technical Article

Factur-X és ZUGFeRD hibrid számlák Delphiben

A konform elektronikus számla nem egy PDF, amelynek az oldalára egy XML fájlt tűztek. Ez egyetlen PDF/A-3 dokumentum, amely kétszer hordozza a számlát: egyszer mint egy oldal, amelyet az ember elolvas, és egyszer mint egy géppel olvasható (machine-readable) Cross Industry Invoice XML, amely a fájlon belül társított fájlként (associated file) van tárolva. A két ábrázolás ugyanazt a számlát írja le. Ez a kettős természet (dual nature) a lényege azoknak a formátumcsaládoknak, amelyeket az európai mandátumok most megkövetelnek: a Factur-X Franciaországban és Németországban, a ZUGFeRD a német nyelvű piacokon, és az XRechnung a német közszféra számlázásánál. Ez a cikk végigveszi, hogyan állít össze a PDFlibPas egy ilyen hibrid számlát Delphiben, hol hagynak teret a szabványok a tévedésre, és miért van szüksége a katalógus (catalog) egyik profiljának egy teljesen különálló XML-építőre (XML builder)

Mi is valójában egy hibrid számla

A látható oldal és a beágyazott XML más-más olvasókat (readers) szolgál ki. A fizetést jóváhagyó ügyintéző a renderelt (megjelenített) oldalt nézi. Egy kötelezettség-nyilvántartó rendszer (accounts-payable system) lenyeli (ingests) az XML-t, strukturált mezőként kiolvassa az összegeket és az adólebontást (tax breakdown), és lekönyveli a tételt anélkül, hogy egy ember bármit is begépelne. Ennek az XML-nek a szemantikai (semantic) tartalmát az EN 16931 szabályozza, az európai szabvány, amely meghatározza a számla adatmodelljét: mely mezők léteznek, mit jelentenek, és melyek kötelezőek (mandatory). Az EN 16931 egy szemantikai modell, nem fájlformátum. A Factur-X, a ZUGFeRD 2.x és az XRechnung mindezt a modellt UN/CEFACT Cross Industry Invoice dokumentumként valósítja meg, ez az a szintaxis, amely az EN 16931 mezőket a hálózaton (on the wire) továbbítja

Ahhoz, hogy a dokumentum archiválható és önleíró (self-describing) is legyen, a konténer (container) a PDF/A-3, amelyet az ISO 19005-3 definiál. A PDF/A-3 az a megfelelőségi szint (conformance level), amely tetszőleges beágyazott fájlokat engedélyez, és a számla XML-nek pontosan ilyennek kell lennie. A PDF/A-2 tiltja olyan fájlok beágyazását, amelyek maguk nem PDF/A formátumúak, tehát egy Factur-X számla nem lehet PDF/A-2. A PDF/A-3 választása ezért nem preferencia, hanem követelmény, amely egyenesen abból következik, hogy nem PDF adatokat (non-PDF data) akarunk beágyazni egy archiválási dokumentumba

Miért Alternative a kapcsolat (relationship)

A bájtok beágyazása a könnyű rész. Az ISO 32000 §7.11.4 határozza meg a beágyazott fájl adatfolyamot (embedded file stream), azt az objektumot, amely a nyers XML-t és annak paramétereit tárolja. Az a rész, amely a fájlt érvényes társított fájllá (associated file) teszi, a §14.13, amely hozzáadja a társított fájl fogalmát és az /AFRelationship kulcsot (key). Ez a kulcs mondja ki, hogyan viszonyulnak a beágyazott adatok ahhoz a tartalomhoz, amelyhez csatolva vannak, és az az érték, amelyet a Factur-X előír (mandates), az Alternative

A választás azért számít, mert a többi érték valami hamisat állítana a dokumentumról. A Source (Forrás) azt jelentené, hogy az XML az az anyag, amelyből a látható tartalmat generálták, egy mester (master), amelyből az oldal származik (derives). A Supplement (Kiegészítés) azt jelentené, hogy az XML az oldal által mutatottakon túl további információkat ad hozzá, egy olyan extrát, amelyet a renderelés nem tartalmaz. Egy Factur-X számlára egyik sem igaz. Az XML és az oldal egy számla két egyenértékű kifejeződése, ugyanazt a jogi tartalmat (legal content) hordozva két formában. Az Alternative az az érték, amely pontosan ezt mondja: a látható tartalom egyenértékű, alternatív reprezentációja. Egy validátor (validator), amely bármilyen más kapcsolatot olvas egy Factur-X fájlon, elutasítja azt, méghozzá jogosan, mert a kapcsolat egy géppel olvasható (machine-readable) állítás arról, hogy mire való a melléklet (attachment)

A profilkatalógus (profile catalog)

A PDFlibPas-hoz mellékelt (ships with) e-számla (E-Invoice) minta ugyanazt a generálási útvonalat hajtja végre (drives) hat profilon keresztül, amelyeket rekordok tömbjeként (array of records) definiálnak az InvoiceModel.pas fájlban. Minden profil hordozza az írónak (writer) szükséges értékeket: a megjelenítendő nevet (display name), a beágyazott fájl nevét (embedded file name), a megfelelőségi szintet (conformance level), az /AFRelationship-et, a verziót, az opcionális országkódot (country code), és a GuidelineID URN-t, amelyet az XML bejelent a dokumentumkörnyezetén (document context) belül

A hat profil: Factur-X EN16931, Factur-X BASIC, Factur-X EXTENDED Franciaországnak, XRechnung 3.0, ZUGFeRD 1.0 COMFORT és ZUGFeRD 2.0 BASIC. A GuidelineID az a mező, amely pontosan megmondja a fogadónak (receiver), hogy melyik profilra számítson, és az értékek specifikusak. A Factur-X EN16931 bejelenti (announces) a urn:cen.eu:en16931:2017 értéket. Az XRechnung 3.0 bejelenti a urn:cen.eu:en16931:2017#compliant#urn:xeinkauf.de:kosit:xrechnung_3.0 értéket. A ZUGFeRD 2.0 BASIC bejelenti a urn:cen.eu:en16931:2017#compliant#urn:zugferd.de:2p0:basic értéket. A beágyazott fájl neve szintén a szerződés (contract) része. A Factur-X profilok beágyazzák a factur-x.xml fájlt, az XRechnung beágyazza az xrechnung.xml fájlt, a ZUGFeRD profilok pedig beágyazzák a ZUGFeRD-invoice.xml vagy a zugferd-invoice.xml fájlt. A fogadó végigpásztázza (scans) a mellékletek neveit (attachment names), hogy megtalálja a számlát, így a fájlnév nem kozmetikai jellegű

A katalógus egyik részletét érdemes alaposan elolvasni. A legtöbb profil az Alternative kapcsolatot használja, de az XRechnung 3.0 bejegyzés a mintában (sample) a Source értéket használja. A két formátum más validátoroknak (validators) és konvencióknak felel meg, és a minta az egyes profilok kapcsolatát a katalógusból (catalog) állítja be, ahelyett, hogy bedrótozna (hard-coding) egyetlen értéket, és emiatt létezik a profilonkénti (per-profile) mező a konstans helyett

A ZUGFeRD 1.0 csapdája (trap)

Csábító azt feltételezni, hogy minden profil az EN 16931 Cross Industry Invoice, kisebb eltérésekkel abban, hogy hány opcionális mezőt (optional fields) tölt fel (populate). Ez a hatból ötre igaz. A ZUGFeRD 1.0 COMFORT esetében azonban nem érvényes, és ennek az oka strukturális (structural), nem pedig kozmetikai

A modern profilok egy UN/CEFACT Cross Industry Invoice-t bocsátanak ki :100 névtér verzióval (namespace version), amelynek gyökéreleme (root element) a rsm:CrossIndustryInvoice. A ZUGFeRD 1.0 megelőzi (predates) ezt a sémát (schema). Ez a 2014-es CrossIndustryDocument :1p0 névtér verzióval, és gyökéreleme a rsm:CrossIndustryDocument. A névtér URN-jei különböznek, a gyökérelem különbözik, és az elemfa (element tree) végig különbözik: az :1p0 séma az adatokat az ApplicableSupplyChainTradeAgreement, ApplicableSupplyChainTradeDelivery és ApplicableSupplyChainTradeSettlement alá csoportosítja, míg a :100 az ApplicableHeaderTradeAgreement, ApplicableHeaderTradeDelivery és ApplicableHeaderTradeSettlement elemeket használja. Az elnevezés (naming) elég hasonló ahhoz, hogy félrevezető legyen, és elég különböző ahhoz, hogy törést (break) okozzon

A profil nevében lévő COMFORT szó azt írja le, hogy mennyire gazdag az adat – ez egy automatizálási minőségű (automation-grade) profil, teljes tételsorokkal (line items), adólebontással (tax breakdown) és fizetési feltételekkel (payment terms) –, nem pedig azt, hogy melyik séma hordozza azt. Tehát nem vehet egy :100 dokumentumot, hogy átcímkézze (relabel) ZUGFeRD 1.0-ra. A minta (sample) ezt úgy kezeli, hogy minden profilrekordon (profile record) elhelyez egy jelzőt (flag), és két külön építőfüggvényt (builder functions) használ, kiválasztva a megfelelőt, mielőtt bármilyen XML generálásra kerülne

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;

A szétválasztás nem csak egy implementációs finomság (implementation nicety). Ha egy :100 fát táplál be (feeding) egy ZUGFeRD 1.0 fogadóba, az egy olyan dokumentumot eredményez, amely a gyökérelemnél (root element) elbukik a sémaellenőrzésen (schema validation), tehát a két családot (families) olyan kódnak kell felépítenie, amely tudja, melyiket is írja éppen

A PDF/A-3 szint kiválasztása

A PDF/A-3 három megfelelőségi szinttel (conformance levels) rendelkezik, és a PDFlibPas a SetPDFAMode segítségével választja ki őket. Az 5-ös mód a PDF/A-3b, az a szint, amely garantálja a megbízható vizuális reprodukciót (visual reproduction). A 6-os mód a PDF/A-3a, amely hozzáadja az a-szint címkézett struktúrára (tagged-structure) és akadálymentesítésre (accessibility) vonatkozó követelményeit. A 7-es mód a PDF/A-3u, amely megköveteli, hogy minden szöveget leképezzenek (mapped) a Unicode-ra. Az üzemmód (mode) engedélyezése beágyazza a könyvtár beépített sRGB kimeneti szándékát (output intent) is, azt a színjellemzést (color characterization), amelyet a PDF/A megkövetel, hogy a megjelenített (rendered) szín definiált legyen, ahelyett, hogy eszközfüggő (device-dependent) lenne

A legtöbb számlafolyam (invoice flows) 3b-n fut, ami elegendő a hű látható oldalhoz, plusz a beágyazott XML-hez. Ha egy explicit ICC profilra van szüksége a beépített helyett, a LoadOutputIntentProfile lecseréli (swaps it in) azt a mód beállítása után. A minta ezen a módon tölti be a lerakat (repository) sRGB profilját, és visszatér a beépített szándékra (built-in intent), ha a fájl nem elérhető (reachable), így a kimeneti szándék (output intent) mindig jelen van

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;

A hibrid számla felépítése

A konténer konfigurálása után a többi három sorrendi lépés (steps in order): a PDF/A-3 mód beállítása, az ember által olvasható oldal megrajzolása, majd az XML csatolása (attach) társított fájlként (associated file). A látható oldal egy közönséges tartalom (ordinary content). Az egyetlen megjegyzendő korlátozás (constraint), hogy a PDF/A tiltja a nem beágyazott Standard 14 betűtípusokat, ezért az oldalnak egy valódi betűképet (real font face) kell beágyaznia (embed), ahelyett, hogy egy beépítettre hivatkozna (reference)

A csatolás (attachment) egyetlen hívás. Az AddFacturXAssociatedFileFromString fogadja (takes) a nyers UTF-8 XML bájtokat és a profil metaadatait, kiírja a beágyazott fájl adatfolyamát (embedded file stream), regisztrálja azt a Katalógus /AF tömbjében (array), amelyet a PDF/A-3 megkövetel, alkalmazza az /AFRelationship-et, és előállítja (generates) az XMP e-számla metaadatokat, amelyek a dokumentumot Factur-X, ZUGFeRD vagy XRechnung fájlként azonosítják. Azt is ellenőrzi, hogy az XML irányelv (guideline) azonosítója megegyezik-e a kért megfelelőségi szinttel (conformance level), így az Ön által felépített XML és a megnevezett profil közötti eltérés elcsíphető (caught), nem pedig csendben elszállítva

// 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);

Az adatútvonalon egy finomság a kódolás (encoding). A beágyazott XML az encoding="UTF-8" értéket deklarálja, a metódus pedig AnsiString-ként fogadja (takes) a bájtjait, így egy nem ASCII eladó- vagy vevőnévnek nyers UTF-8 oktettként (octets) kell elérnie a hívást. Egy egyszerű kényszerítés (cast) a rendszer ANSI kódlapján keresztül megrongálná (corrupt) ezeket a karaktereket, és csendben olyan számlát eredményezne, amelynek XML-je már nem egyezik meg a saját deklarációjával. A minta (sample) explicit módon UTF-8-ra kódol, mielőtt átadná a bájtokat, ami biztonságos módja annak, hogy bármely bájtorientált (byte-oriented) PDF API-t egy Unicode string-ből tápláljunk (feed)

Az olyan XML csatolására, amely nem elismert (recognized) e-számla profil, az AddPDFA3AssociatedFileFromString az általános megfelelő (generic counterpart). Fájlnevet, MIME-típust, leírást (description), kapcsolatot (relationship) és bájtokat fogad, és megír egy egyszerű PDF/A-3 társított fájlt anélkül, hogy számlaspecifikus (invoice-specific) metaadatokat vagy irányelvi (guideline) ellenőrzéseket végezne. Használja kiegészítő adatokhoz (supplementary data); számlákhoz használja a Factur-X metódust, így a profilmetaadatok és az irányelvek egyeztetése meg lesz írva az Ön számára

A dokumentum előállítása után a következő kérdés az, hogy átmegy-e a PDF/A és az akadálymentesítési (accessibility) validáláson, és hogy aláírható-e anélkül, hogy megtörné a megfelelést (compliance). Ezeket a PDF/A és a PDF/UA preflight bemutatásában, valamint a megfelelőségi és aláírási munkapadban ismertetjük. Mindez a PDFlibPas Delphi PDF Library részeként érkezik (ships), a PDF/A, a címkézési (tagging) és a dokumentum-tulajdonság (document-property) API-k mellett, amelyekre az e-számla útvonal (path) épül