Teknisk artikel

Factur-X och ZUGFeRD hybridfakturor i Delphi

En elektronisk faktura (e-invoice) som uppfyller kraven är inte en PDF med en XML-fil fastnitad (stapled) på sidan. Det är ett enda PDF/A-3-dokument som bär på fakturan två gånger: en gång som en sida en människa läser, och en gång som en maskinläsbar Cross Industry Invoice XML lagrad inuti filen som en associerad fil (associated file). De två representationerna beskriver samma faktura. Den dubbla naturen är hela poängen med de formateringsfamiljer som europeiska mandat nu kräver, Factur-X i Frankrike och Tyskland, ZUGFeRD tvärs över tyskspråkiga marknader, och XRechnung för fakturering till tysk offentlig sektor. Denna artikel går igenom hur PDFlibPas sätter ihop (assembles) en sådan hybridfaktura i Delphi, var standarderna lämnar utrymme att göra fel, och varför en profil i katalogen behöver en helt separat XML-byggare (XML builder)

Vad en hybridfaktura egentligen är

Den synliga sidan och den inbäddade XML:en tjänar olika läsare. En handläggare som godkänner en betalning tittar på den renderade sidan. Ett leverantörsreskontrasystem (accounts-payable system) tar in (ingests) XML:en, läser totalerna och skattefördelningen (tax breakdown) som strukturerade fält, och bokför posten utan att en människa matar in någonting. Det semantiska innehållet i den XML:en styrs av EN 16931, den europeiska standarden som definierar fakturans datamodell: vilka fält som finns, vad de betyder, och vilka som är obligatoriska. EN 16931 är en semantisk modell, inget filformat. Factur-X, ZUGFeRD 2.x och XRechnung realiserar alla den modellen som ett UN/CEFACT Cross Industry Invoice-dokument, syntaxen som bär EN 16931-fälten över kabeln (on the wire)

För att dokumentet ska vara både arkiverbart och självbeskrivande är behållaren PDF/A-3, definierad av ISO 19005-3. PDF/A-3 är den efterlevnadsnivå (conformance level) som tillåter godtyckliga inbäddade filer (arbitrary embedded files), vilket är precis vad en faktura-XML behöver vara. PDF/A-2 förbjuder inbäddning av filer som inte själva är PDF/A, så en Factur-X-faktura kan inte vara PDF/A-2. Valet av PDF/A-3 är därför inte en preferens, det är ett krav som direkt följer av viljan att bädda in icke-PDF-data i ett arkivdokument

Varför relationen (relationship) är Alternative

Att bädda in byten är den enkla delen. ISO 32000 §7.11.4 definierar strömmen för den inbäddade filen, det objekt som håller den råa XML:en och dess parametrar. Den del som gör filen till en giltig associerad fil är §14.13, som lägger till begreppet en associerad fil (associated file) och nyckeln /AFRelationship. Den nyckeln anger hur den inbäddade datan relaterar till det innehåll den är fäst (attached) vid, och det värde som Factur-X dikterar (mandates) är Alternative

Valet spelar roll eftersom de andra värdena skulle påstå något falskt om dokumentet. Source skulle betyda att XML:en är det material utifrån vilket det synliga innehållet genererades, ett original som sidan härleds från. Supplement skulle betyda att XML:en lägger till information utöver vad sidan visar, ett tillägg som inte ryms (not contained) i renderingen. Ingetdera är vad en Factur-X-faktura är. XML:en och sidan är två likvärdiga uttryck för en och samma faktura, som bär på samma juridiska innehåll i två former. Alternative är det värde som säger exakt det: en likvärdig alternativ representation av det synliga innehållet. En validator som läser in någon annan relation på en Factur-X-fil kommer att underkänna den, och med all rätt, eftersom relationen är ett maskinläsbart påstående om vad bilagan är till för

Profilkatalogen

E-Invoice-exemplet som skeppas med PDFlibPas driver samma genereringssökväg (generation path) över sex profiler, definierade som en array av record (records) i InvoiceModel.pas. Varje profil bär på de värden som skrivaren (writer) behöver: ett visningsnamn, det inbäddade filnamnet, en efterlevnadsnivå (conformance level), /AFRelationship, en version, en valfri landskod, och den GuidelineID-URN som XML:en tillkännager inuti sin dokumentkontext (document context)

De sex är Factur-X EN16931, Factur-X BASIC, Factur-X EXTENDED för Frankrike, XRechnung 3.0, ZUGFeRD 1.0 COMFORT och ZUGFeRD 2.0 BASIC. GuidelineID är fältet som berättar för en mottagare precis vilken profil man kan förvänta sig, och värdena är specifika. Factur-X EN16931 tillkännager urn:cen.eu:en16931:2017. XRechnung 3.0 tillkännager urn:cen.eu:en16931:2017#compliant#urn:xeinkauf.de:kosit:xrechnung_3.0. ZUGFeRD 2.0 BASIC tillkännager urn:cen.eu:en16931:2017#compliant#urn:zugferd.de:2p0:basic. Det inbäddade filnamnet är en del av kontraktet (contract) det med. Factur-X-profiler bäddar in factur-x.xml, XRechnung bäddar in xrechnung.xml, och ZUGFeRD-profilerna bäddar in ZUGFeRD-invoice.xml eller zugferd-invoice.xml. En mottagare skannar bilagornas namn för att hitta fakturan, så filnamnet är inte kosmetiskt

En detalj i katalogen är värd att läsas noggrant. De flesta profiler använder relationen Alternative, men XRechnung 3.0-posten i exemplet använder Source. De två formaten svarar (answer) inför olika validatorer och konventioner, och exemplet sätter varje profils relation från katalogen snarare än att hårdkoda ett enda värde, vilket är varför fältet finns per profil i stället för som en konstant

Fällan med ZUGFeRD 1.0

Det är frestande att anta att varje profil är en EN 16931 Cross Industry Invoice med smärre variationer i hur många valfria fält man fyller i (populate). Det gäller för fem av de sex. Det gäller inte för ZUGFeRD 1.0 COMFORT, och orsaken är strukturell snarare än kosmetisk

De moderna profilerna genererar (emit) en UN/CEFACT Cross Industry Invoice med namnrymdsversion (namespace version) :100, vars rotelement (root element) är rsm:CrossIndustryInvoice. ZUGFeRD 1.0 kom före det schemat (predates that schema). Det är 2014 års CrossIndustryDocument med namnrymdsversion :1p0, och dess rotelement är rsm:CrossIndustryDocument. Namnrymds-URN:erna (namespace URNs) skiljer sig åt, rotelementet skiljer sig, och elementträdet (element tree) skiljer sig rakt igenom: schema :1p0 grupperar data under ApplicableSupplyChainTradeAgreement, ApplicableSupplyChainTradeDelivery och ApplicableSupplyChainTradeSettlement, medan :100 använder ApplicableHeaderTradeAgreement, ApplicableHeaderTradeDelivery och ApplicableHeaderTradeSettlement. Namngivningen är tillräckligt lik för att vilseleda och tillräckligt annorlunda för att gå sönder (break)

Ordet COMFORT i profilnamnet beskriver hur rik datan är, en automations-klassad (automation-grade) profil med fullständiga orderrader (line items), skattefördelning (tax breakdown) och betalningsvillkor, inte vilket schema som bär på den. Så du kan inte ta ett :100-dokument och etikettera om det (relabel it) för ZUGFeRD 1.0. Exemplet hanterar detta med en flagga på varje profil-record och två separata byggarfunktioner (builder functions), som väljer den rätta innan någon XML genereras

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;

Uppdelningen (split) är ingen implementationsfiness (implementation nicety). Att mata (Feeding) ett :100-träd till en ZUGFeRD 1.0-mottagare producerar ett dokument som fallerar vid schemavalidering på rotelementet, så de två familjerna måste byggas med kod som vet vilken av dem den skriver

Att välja PDF/A-3-nivå

PDF/A-3 har tre efterlevnadsnivåer (conformance levels), och PDFlibPas väljer dem genom SetPDFAMode. Läge 5 är PDF/A-3b, nivån som garanterar tillförlitlig visuell återgivning. Läge 6 är PDF/A-3a, vilket lägger till kraven på taggad struktur och tillgänglighet (accessibility) från nivå a. Läge 7 är PDF/A-3u, som kräver att all text mappas (mapped) till Unicode. Att aktivera läget bäddar också in bibliotekets inbyggda sRGB output intent (utdataintention), den färgkarakterisering som PDF/A kräver så att renderad färg är definierad i stället för enhetsberoende (device-dependent)

De flesta fakturaflöden (invoice flows) körs på 3b, vilket är tillräckligt för en naturtrogen (faithful) synlig sida plus den inbäddade XML:en. Om du behöver en explicit ICC-profil i stället för den inbyggda, byter LoadOutputIntentProfile in den (swaps it in) efter att läget (mode) är satt. Exemplet laddar repots sRGB-profil på det här sättet och faller tillbaka på den inbyggda intentionen när filen inte kan nås (reachable), så att ett output intent alltid finns närvarande

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;

Att bygga hybridfakturan

Med behållaren konfigurerad (container configured) består resten av tre steg i ordning: ställ in PDF/A-3-läget, rita den läsbara (human-readable) sidan, fäst sedan XML:en som en associerad fil. Den synliga sidan är ordinärt innehåll. Det enda villkor värt att komma ihåg är att PDF/A förbjuder de icke-inbäddade (non-embedded) Standard 14-typsnitten, så sidan måste bädda in en äkta typsnittsfamilj (real font face) i stället för att referera till en inbyggd

Bifogandet (attachment) är ett enda anrop. AddFacturXAssociatedFileFromString tar de råa UTF-8-XML-byten plus profilmetadatan, skriver den inbäddade filströmmen, registrerar den i katalogens (Catalog) /AF-array som PDF/A-3 kräver, applicerar /AFRelationship och genererar den XMP e-fakturameta (XMP e-invoice metadata) som identifierar dokumentet som Factur-X, ZUGFeRD eller XRechnung. Den kontrollerar också att XML:ens guideline ID matchar efterlevnadsnivån (conformance level) du bad om, så en missmatchning mellan XML:en du byggde och profilen du namngav fångas i stället för att tyst skickas i väg (shipped)

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

En spetsfundighet (subtlety) i datasökvägen är kodningen (encoding). Den inbäddade XML:en deklarerar encoding="UTF-8", och metoden tar sina bytes som en AnsiString, så ett icke-ASCII säljar- eller köparnamn måste nå anropet som råa UTF-8-oktetter (raw UTF-8 octets). En vanlig typomvandling (plain cast) genom systemets ANSI code page skulle korrumpera dessa tecken och tyst producera en faktura vars XML inte längre matchar dess egen deklaration. Exemplet kodar till UTF-8 explicit innan byten räcks över, vilket är det säkra sättet att mata (feed) något byte-orienterat PDF-API från en Unicode-string

För att fästa XML som inte är en igenkänd e-fakturaprofil (e-invoice profile), är AddPDFA3AssociatedFileFromString den generiska motsvarigheten (generic counterpart). Den tar ett filnamn, MIME-typ, beskrivning, relation och bytes, och skriver en ren (plain) PDF/A-3 associerad fil utan någon fakturaspecifik metadata eller riktlinjekontroll (guideline checks). Använd den för kompletterande data (supplementary data); använd Factur-X-metoden för fakturor, så att profilmetadatan och riktlinjematchningen skrivs åt dig

Så snart dokumentet är producerat, är nästa frågor huruvida det klarar PDF/A- och tillgänglighetsvalidering, och ifall det kan signeras utan att bryta efterlevnaden. Det täcks in i PDF/A och PDF/UA preflight-genomgången samt arbetsbänken för efterlevnad och signering. Allt detta skeppas som en del av PDFlibPas Delphi PDF Library, jämsides med API:erna för PDF/A, taggning och dokumentegenskaper (document-property) som e-fakturasökvägen (e-invoice path) bygger på