Technical Article

Validering av e-fakturor: veraPDF och Mustang i Delphi

En Factur-X- eller ZUGFeRD-faktura är två dokument som bär ett gemensamt filnamn. Det yttre dokumentet är en PDF/A-3-container som en arkivläsare måste acceptera under de kommande tio åren. Det inre dokumentet är en XML-faktura som en köpares bokföringssystem måste tolka (parse) mot EN 16931. Det misstag som skickar trasiga fakturor ut i produktion är att tro att om man får det första rätt, så får man det andra på köpet. Det gör man inte. En fil kan vara en fläckfri PDF/A-3 och ändå bära med sig XML som ingen skattemyndighet kommer att acceptera, och den kan bära XML helt enligt skolboken för EN 16931 inuti en container som fallerar arkivvalideringen. De två lagren valideras av två olika verktyg som inte vet något om varandra, och en verklig pipeline måste tillfredsställa båda

Två validerare, två olika frågor

veraPDF är referensimplementationen för PDF/A. Peka den på en faktura och den svarar på en fråga: är detta en konform PDF/A-3-fil? Den kontrollerar de saker ISO 19005-3 bryr sig om. Är varje typsnitt inbäddat? Finns det en OutputIntent? Deklarerar XMP-metadatan rätt del och efterlevnadsnivå? För en e-faktura kontrollerar den också de tillhörande fil-infrastrukturer (associated-file plumbing) som PDF/A-3 kräver, eftersom XML-filen åker med som en inbäddad fil med en /AFRelationship och en post i dokumentkatalogens /AF-fält. veraPDF säger ingenting om huruvida fakturatotalen stämmer, eftersom det inte ligger inom dess ansvarsområde

Mustang är en öppen-källkodsvaliderare från Mustangproject. Den ställer den ortogonala frågan: är den inbäddade XML-filen en giltig faktura? Den kör XML-filen mot schemat för den deklarerade profilen och tillämpar sedan affärsreglerna för EN 16931 samt de landsspecifika regeluppsättningar som ligger ovanpå, bland dem CIUS för XRechnung. Den kontrollerar att ett säljar-MOM-nummer finns när totalsummorna kräver det, att rabatt- och avgiftsbelopp stämmer överens med dokumentets totalsumma, och att profilens URN i XML-filen stämmer med vad filen utger sig för att vara. Mustang bryr sig inte om huruvida den omgivande PDF-filen bäddar in sina typsnitt, eftersom det är veraPDFs jobb

Inget av verktygen är en superset av det andra. veraPDF godkänner en strukturellt perfekt container runt en nonsens-XML. Mustang godkänner perfekt XML inlindad i en container med saknad OutputIntent. Båda fångar exakt den klass av fel som den andra är blind för, vilket är hela anledningen till att en seriös valideringsrigg (validation harness) kör båda och behandlar en fil som redo att skickas enbart när båda är överens

Valideringsmatrisen

För att bevisa att biblioteket producerar filer som överlever båda grindarna bygger valideringsriggen en matris. Sex fakturaprofiler täcker det spektrum som en europeisk pipeline möter i praktiken: Factur-X EN 16931, Factur-X BASIC, varianten Factur-X EXTENDED France B2B, XRechnung 3.0, ZUGFeRD 1.0 COMFORT och ZUGFeRD 2.0 BASIC. Varje profil genereras mot två nivåer av PDF/A-underkonformitet, 3b och 3u, eftersom kraven för nivå B och nivå U divergerar ifråga om Unicode-mappning och en fil som klarar den ena kan misslyckas på den andra. Sex profiler gånger två nivåer är tolv filer, var och en av dem byggd "headless" via exakt samma kodbana som GUI-exemplet levereras med, så artefakterna som testas är inte handtrimmade för testet

Generatorn skriver alla tolv, och ett skript matar var och en av dem till båda validerarna. På den första fulla körningen godkände veraPDF alla tolv. Containerinfrastrukturen var korrekt över hela linjen: tillhörande filer var registrerade, XMP-konformitet deklarerad, output-intents på plats. Mustang godkände åtta. Fyra fakturor var strukturellt giltiga PDF/A-3-filer som bar på XML som affärsregelvalideraren förkastade, vilket är exakt den uppdelning som tvåverktygsstrategin existerar för att lyfta fram. Om riggen hade litat enbart på veraPDF skulle de fyra ha verkat klara att skickas

De två rättelserna som slöt gapet

De fyra felen i Mustang berodde på två olika orsaker, och rättelsen för varje är en detalj värd att känna till innan du genererar dessa profiler själv

Den första gällde profilen Factur-X EXTENDED France B2B. Den ursprungliga generatorn skickade in en intern etikett som efterlevnadsnivå och en intern URN som riktlinje (guideline), varpå Mustang avvisade filen med ett fel om ogiltigt konformitetsvärde följt av ett fel om att profiltypen saknade stöd. Anledningen är att XMP-fältet fx:ConformanceLevel inte är en fritt utbytbar textlucka för att namnge din egen profil. Factur-X definierar exakt fem standardvärden för den: MINIMUM, BASIC WL, BASIC, EN 16931, och EXTENDED. En Frankrike-specifik B2B-faktura är fortfarande ett dokument av typen EXTENDED vad beträffar XMP-metadatan. Den franska karaktären hos fakturan uttrycks inte genom att man uppfinner ett sjätte konformitetsvärde. Den uttrycks av landskoden, FR, och av riktlinjens identifierare inuti XML-filen, som måste bära prefixet urn:cen.eu:en16931:2017#conformant# vilket markerar en CIUS i överensstämmelse med EN 16931. Att skicka standardvärdet EXTENDED med FR som landskod och den korrekta riktlinje-URN:en gjorde filen konform (conformant)

I bibliotekets API görs det genom ett anrop till AddFacturXAssociatedFileFromString där efterlevnad, land och riktlinje är synkroniserade. Argumentet för efterlevnadsnivå bär på standardtecknet, argumentet för landskod bär på FR, och riktlinjens URN ligger i de XML-bytes du skickar in

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;

Den andra orsaken låg i profilen ZUGFeRD 1.0 COMFORT, och den hade ingenting med metadata att göra. ZUGFeRD 1.0 valideras mot :1p0-XSD, som är strängare gällande kardinalitet (cardinality) än de beskrivande sammanfattningarna antyder. Den XSD:n kräver att summeringen av avräkning i huvudet, ram:SpecifiedTradeSettlementMonetarySummation, innehåller ram:ChargeTotalAmount och ram:AllowanceTotalAmount exakt en gång vardera. Den genererade XML:en saknade båda, så Mustang rapporterade att elementen måste förekomma exakt en gång. Dessa är inte valfria när schemat säger att minOccurs är ett. Genom att mata ut båda i den ordning schemat angav, omedelbart efter ram:LineTotalAmount, med ett värde på 0.00 när det inte finns några avgifter eller rabatter, uppfylldes schemat. En nolla är ett närvarande element; ett frånvarande element är ett schemafel. Med de två rättelserna på plats gick matrisen till tolv av tolv på Mustang, medan den förblev tolv av tolv på veraPDF

XRechnung-fälten som vänder ogiltigt till giltigt

XRechnung förtjänar en egen notis eftersom dess tyska CIUS lägger till affärsregler som saknas i basuppsättningen för EN 16931, och de misslyckas på sätt som vid en första anblick får det att verka som att inget är fel med dokumentet. Två av dem rör elektroniska adresser. BT-34 är säljarens elektroniska adress och BT-49 är köparens elektroniska adress, vilka utgör ruttändpunkterna (routing endpoints) som en tysk offentlig portal använder för att leverera och bekräfta fakturan. Den grundläggande EN 16931-modellen behandlar dem som valfria. Det gör inte XRechnung. Utelämna endera och fakturan är välformaterad (well-formed), giltig mot schemat, men avvisas

Den tredje är regeln BR-DE-6, som kräver att säljarens kontakttelefonnummer finns med. Det är den typ av fält en utvecklare utelämnar eftersom det känns som en presentation snarare än som data, och dess frånvaro producerar ett valideringsfel som pekar på säljarens kontaktgrupp snarare än på något uppenbart saknat. Att tillhandahålla BT-34, BT-49 och säljarens telefonnummer är det som flyttar en XRechnung-fil från ogiltig till giltig under Mustang, och ingenting av detta ändrar något veraPDF ser, eftersom alla tre bor i XML-filen

Att koppla bibliotekets utdata till en validerare

Den arkitektoniska poängen med riggen kan generaliseras till vilket affärssystem som helst. PDF-biblioteket skriver en konform container och bäddar in XML-filen. Det är inte, och bör inte försöka vara, auktoriteten för affärsreglerna i EN 16931. ValidateFacturXInvoice i biblioteket kontrollerar containerns konsekvens, att katalogens /AF-fält, de inbäddade filernas namnträd (name tree), XMP-attributet DocumentFileName, profilen, riktlinjen (guideline), och /AFRelationship alla stämmer överens, men den validerar inte skattekoder och stämmer inte av belopp. Den rätta arbetsfördelningen är att låta affärssystemet extrahera XML:en och överlämna den till en dedikerad fakturavaliderare, precis så som testriggen lämnar över den till Mustang

Att läsa filen tillbaka talar om för dig vad som faktiskt skrevs. DetectFacturXInvoice rapporterar huruvida en faktura kändes igen, och GetFacturXInvoiceInfo läser metadatafälten via en tagg: tagg 1 är det inbäddade filnamnet, tagg 2 är XMP:s DocumentFileName, tagg 5 är efterlevnadsnivån, tagg 6 är riktlinjeidentifieraren (guideline identifier), och tagg 7 är /AFRelationship. Att bekräfta att efterlevnadsnivån du läser tillbaka är standardtecknet (token) och inte en intern etikett är det billigaste sättet att fånga EXTENDED-misstaget innan en fil lämnar din bygge (build)

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;

ExtractFacturXXMLToString returnerar de råa XML-bytesen som en AnsiString, redo att skrivas till en fil eller strömmas in i en validerarprocess. I testriggen är målet Mustang, anropad via sin kommandorads-jar, med veraPDF körd i samma pass över samma fil. Kopplingen är liten: en konsolgenerator, EInvoiceValidation.dpr, skriver de tolv filerna med hjälp av den gemensamma fakturamodellen från exemplet, och ett skript, run-validation.ps1, driver båda validerarna över utdatakatalogen och skriver ut en godkännande- och underkännandetabell. Samma tvåstegsform, generera med biblioteket och verifiera med externa validerare, är vad ett kontinuerligt integrationsjobb bör köra vid varje ändring av fakturagenereringen, eftersom det enda sättet att veta att en fil tillfredsställer båda lagren är att fråga båda verktygen

Om din pipeline också måste certifiera containern innan signering, så behandlas preflight-delen av detta arbete i vår genomgång om PDF/A- och PDF/UA-preflight i Delphi, och det bredare certifiera-sedan-signera-flödet beskrivs i arbetsbänken för efterlevnad och signering. Båda bygger på samma genereringsväg som levereras som en del av Delphi PDF Library för Delphi och C++Builder, vid sidan av API:erna för PDF/A, tillhörande filer (associated-file) och metadata som används här