En konform elektronisk faktura är inte en PDF med en XML-fil häftad på sidan. Det är ett enskilt PDF/A-3-dokument som bär 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 tillhörande fil (associated file). De två representationerna beskriver samma faktura. Denna dubbla natur är hela poängen med de formatfamiljer som europeiska mandat numera kräver, Factur-X i Frankrike och Tyskland, ZUGFeRD över tysktalande marknader, och XRechnung för tyska offentliga fakturor. Denna artikel går igenom hur PDFlibPas sätter ihop en sådan hybridfaktura i Delphi, var standarderna lämnar utrymme för att göra fel, och varför en profil i katalogen behöver en helt separat XML-byggare
Vad en hybridfaktura faktiskt ä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 läser in XML:en, läser av totalsummor och momsuppdelning som strukturerade fält, och bokför posten utan att en människa knappar 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 existerar, vad de betyder, och vilka som är obligatoriska. EN 16931 är en semantisk modell, inte ett filformat. Factur-X, ZUGFeRD 2.x, och XRechnung förverkligar alla den modellen som ett UN/CEFACT Cross Industry Invoice-dokument, den syntax som bär EN 16931-fälten över nätverket
För att dokumentet ska vara både arkiverbart och självbeskrivande är containern PDF/A-3, definierad av ISO 19005-3. PDF/A-3 är den efterlevnadsnivå som tillåter godtyckliga inbäddade filer, vilket är exakt vad en faktura-XML måste 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 följer direkt av viljan att bädda in icke-PDF-data i ett arkivdokument
Varför relationen är Alternative
Att bädda in byten är den enkla delen. ISO 32000 §7.11.4 definierar den inbäddade filströmmen, det objekt som håller den råa XML:en och dess parametrar. Den del som gör filen till en giltig tillhörande fil (associated file) är §14.13, som lägger till konceptet en tillhörande fil och /AFRelationship-nyckeln. Den nyckeln anger hur de inbäddade datan relaterar till det innehåll den är fäst vid, och det värde Factur-X kräver är Alternative
Valet spelar roll eftersom de andra värdena skulle hävda något falskt om dokumentet. Source skulle betyda att XML:en är det material från vilket det synliga innehållet genererades, en master som sidan härrör från. Supplement skulle betyda att XML:en lägger till information utöver det sidan visar, ett extra tillägg som inte finns i renderingen. Ingetdera stämmer för en Factur-X-faktura. XML:en och sidan är två ekvivalenta uttryck för en faktura, som bär samma legala innehåll i två former. Alternative är värdet som säger exakt det: en ekvivalent alternativ representation av det synliga innehållet. En validerare som läser in någon annan relation på en Factur-X-fil kommer att avvisa den, och med rätta, eftersom relationen är ett maskinläsbart påstående om vad bilagan är till för
Profilkatalogen
E-fakturaexemplet (The E-Invoice sample) som levereras med PDFlibPas driver samma genereringsväg över sex profiler, definierade som en array av poster (records) i InvoiceModel.pas. Varje profil bär de värden som skrivaren behöver: ett visningsnamn, det inbäddade filnamnet, en efterlevnadsnivå, /AFRelationship, en version, en valfri landskod, och den GuidelineID-URN som XML:en meddelar inuti sin dokumentkontext
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 talar om för en mottagare exakt vilken profil de kan förvänta sig, och värdena är specifika. Factur-X EN16931 meddelar urn:cen.eu:en16931:2017. XRechnung 3.0 meddelar urn:cen.eu:en16931:2017#compliant#urn:xeinkauf.de:kosit:xrechnung_3.0. ZUGFeRD 2.0 BASIC meddelar urn:cen.eu:en16931:2017#compliant#urn:zugferd.de:2p0:basic. Det inbäddade filnamnet är en del av kontraktet det också. 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 namnen på bilagorna för att hitta fakturan, så filnamnet är inte kosmetiskt
En detalj i katalogen är värd att läsa noggrant. De flesta profiler använder relationen Alternative, men XRechnung 3.0-posten i exemplet använder Source. De två formaten svarar mot olika validerare och konventioner, och exemplet sätter varje profils relation från katalogen istället för att hårdkoda ett enda värde, vilket är anledningen till att det per profil-specifika fältet existerar istället för en konstant
ZUGFeRD 1.0-fällan
Det är frestande att anta att varje profil är EN 16931 Cross Industry Invoice med mindre variationer i hur många valfria fält du fyller i. Det stämmer för fem av de sex. Det stämmer inte för ZUGFeRD 1.0 COMFORT, och orsaken är strukturell snarare än kosmetisk
De moderna profilerna genererar en UN/CEFACT Cross Industry Invoice med namnrymdsversion :100, vars rotelement är rsm:CrossIndustryInvoice. ZUGFeRD 1.0 föregår det schemat. Den använder 2014 års CrossIndustryDocument med namnrymdsversion :1p0, och dess rotelement är rsm:CrossIndustryDocument. Namnrymdens URN:er skiljer sig, rotelementet skiljer sig, och elementträdet skiljer sig rakt igenom: :1p0-schemat 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
Ordet COMFORT i profilnamnet beskriver hur rik datan är, en automationsklassad profil med fulla artikelrader, momsredovisning och betalningsvillkor, inte vilket schema som bär den. Så du kan inte ta ett :100-dokument och märka om det för ZUGFeRD 1.0. Exemplet hanterar detta med en flagga på varje profilpost och två separata byggfunktioner, och 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 är inte en implementeringsfiness. Att mata ett :100-träd till en ZUGFeRD 1.0-mottagare producerar ett dokument som misslyckas med schemavalideringen vid rotelementet, så de två familjerna måste byggas av kod som vet vilken av dem den skriver
Att välja PDF/A-3-nivå
PDF/A-3 har tre efterlevnadsnivåer, 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, som lägger till de taggade struktur- och tillgänglighetskraven från nivå a. Läge 7 är PDF/A-3u, som kräver att all text mappas till Unicode. Att aktivera läget bäddar också in bibliotekets inbyggda sRGB-utmatningsavsikt (output intent), färgkaraktäriseringen som PDF/A kräver så att renderad färg är definierad i stället för enhetsberoende
De flesta fakturaflöden körs på 3b, vilket är tillräckligt för en trogen 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 efter att läget är satt. Exemplet laddar in repots sRGB-profil på detta sätt och faller tillbaka på den inbyggda utmatningsavsikten när filen inte går att nå, så utmatningsavsikten är alltid 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 containern konfigurerad är resten tre steg i ordning: ställ in PDF/A-3-läget, rita den läsbara sidan, och fäst sedan XML:en som en tillhörande fil. Den synliga sidan är vanligt innehåll. Den enda begränsningen värd att komma ihåg är att PDF/A förbjuder de icke-inbäddade Standard 14-typsnitten, så sidan måste bädda in ett riktigt typsnitt (font face) i stället för att referera till ett inbyggt
Bifogandet är ett enda anrop. AddFacturXAssociatedFileFromString tar de råa UTF-8-XML-bytesen plus profilmetadatan, skriver den inbäddade filströmmen, registrerar den i Katalogens /AF-fält som PDF/A-3 kräver, applicerar /AFRelationship, och genererar XMP e-fakturametadatan som identifierar dokumentet som Factur-X, ZUGFeRD, eller XRechnung. Den kontrollerar också att XML:ens guideline-ID stämmer överens med den efterlevnadsnivå du bad om, så ett missmatch mellan den XML du byggde och den profil du namngav fångas i stället för att tyst levereras
// 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 finess i datasökvägen (data path) ä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. En enkel cast genom systemets ANSI-teckentabell skulle korrumpera dessa tecken och tyst producera en faktura vars XML inte längre stämmer överens med sin egen deklaration. Exemplet kodar till UTF-8 explicit innan bytesen lämnas över, vilket är det säkra sättet att mata vilket byte-orienterat PDF-API som helst från en Unicode-string
För att fästa XML som inte är en erkänd e-fakturaprofil är AddPDFA3AssociatedFileFromString den generiska motsvarigheten. Den tar ett filnamn, MIME-typ, beskrivning, relation, och bytes, och skriver en ren PDF/A-3-tillhörande fil utan någon fakturaspecifik metadata eller guideline-kontroller. Använd den för kompletterande data; använd Factur-X-metoden för fakturor, så att profilmetadatan och guideline-matchningen skrivs åt dig
När dokumentet väl är producerat är nästa frågor huruvida det klarar PDF/A- och tillgänglighetsvalidering, och om det kan signeras utan att bryta efterlevnaden. Dessa täcks i genomgången av PDF/A- och PDF/UA-preflight och arbetsbänken för efterlevnad och signering. Allt detta levereras som en del av PDFlibPas Delphi PDF Library, vid sidan av API:erna för PDF/A, taggning och dokumentegenskaper som e-fakturavägen bygger på