技術文章

在 Delphi 中驗證電子發票:veraPDF 與 Mustang

Factur-X 或 ZUGFeRD 發票是披著同一個檔名的兩份文件。外部文件是 PDF/A-3 容器,具備封存用途的閱讀器必須在未來十年內都能接受它。內部文件則是 XML 發票,買方的會計系統必須根據 EN 16931 進行解析。將損壞的發票投入生產環境的錯誤在於,以為只要把第一個做對,第二個就會自然正確。事實並非如此。一個檔案可以是完美無瑕的 PDF/A-3,但依然夾帶任何稅務機關都不會接受的 XML;它也可以在容器內夾帶教科書級別的 EN 16931 XML,卻通不過封存驗證。這兩層是由兩個彼此一無所知的不同工具所驗證,而實際的管線(pipeline)必須同時滿足兩者

兩個驗證器,兩個不同的問題

veraPDF 是 PDF/A 的參考實作。將它指向一張發票,它會回答一個問題:這是一個合規的 PDF/A-3 檔案嗎?它會檢查 ISO 19005-3 所關心的事情。每個字型是否都已內嵌?是否有 OutputIntent?XMP 詮釋資料是否宣告了正確的 part 和 conformance level?對於電子發票,它還會檢查 PDF/A-3 要求的關聯檔案(associated-file)管道,因為 XML 會作為一個帶有 /AFRelationship 以及在文件目錄(document catalog)的 /AF 陣列中擁有一個項目的內嵌檔案隨之傳遞。veraPDF 對於發票總額是否加總正確隻字未提,因為那不在其職責範圍內

Mustang 是來自 Mustangproject 的開源驗證器。它問的是一個正交(orthogonal)的問題:內嵌的 XML 是一張有效的發票嗎?它會根據宣告的設定檔(profile)綱要(schema)來執行 XML,然後套用 EN 16931 業務規則以及疊加在上的特定國家規則集,其中包括 XRechnung 的 CIUS。當總額要求必須提供時,它會檢查賣方增值稅(VAT)識別碼是否存在,折讓和費用金額是否與文件總額相符,XML 中的設定檔 URN 是否與檔案所聲明的相符。Mustang 不關心外圍的 PDF 是否內嵌了字型,因為那是 veraPDF 的工作

這兩個工具都不是彼此的超集。veraPDF 會讓包著無意義 XML 卻結構完美的容器通過。Mustang 會讓包在缺少 OutputIntent 容器中的完美 XML 通過。它們各自精準地捕捉到另一個所看不見的那類缺陷,這就是為什麼一個嚴謹的驗證測試載具(harness)必須同時執行兩者,並且只有當兩者都同意時才將檔案視為可交付的全部原因

驗證矩陣

為了證明函式庫能產出通過這兩道關卡的檔案,測試載具建立了一個矩陣。六個發票設定檔涵蓋了歐洲管線在實務上會遇到的範圍:Factur-X EN 16931、Factur-X BASIC、Factur-X EXTENDED 針對法國 B2B 的變體、XRechnung 3.0、ZUGFeRD 1.0 COMFORT 以及 ZUGFeRD 2.0 BASIC。每個設定檔都會針對兩種 PDF/A 子遵循層級(3b 和 3u)進行生成,因為 B 層級和 U 層級的要求在 Unicode 對應上有所分歧,能通過其中一個的檔案可能會在另一個失敗。六個設定檔乘以兩個層級共十二個檔案,每一個都是由 GUI 範例隨附的相同程式碼路徑在無周邊(headless)模式下建置出來的,因此受測的產出物(artifacts)並非為了測試而人工調整過

產生器寫入了這十二個檔案,而指令碼將它們一一餵給兩個驗證器。在第一次完整執行時,veraPDF 全數通過了這十二個檔案。容器的管道完全正確:關聯檔案已註冊、XMP 遵循已宣告、輸出意圖已就位。Mustang 通過了八個。四張發票是結構有效的 PDF/A-3 檔案,但夾帶的 XML 卻被業務規則驗證器拒絕,而這正是採用雙工具方法的目的所在。如果測試載具只信任 veraPDF,那四個檔案看起來就像是完成品

彌合差距的兩個修正

這四個 Mustang 失敗來自兩個不同的原因,而針對每一個的修正都是您自己生成這些設定檔之前值得了解的細節

第一個是 Factur-X EXTENDED 法國 B2B 設定檔。最初的產生器傳入了一個內部標籤作為遵循層級,並傳入了一個內部 URN 作為指南(guideline),而 Mustang 拒絕了這個檔案,先是報了 invalid-conformance-value 錯誤,接著又報了 unsupported-profile-type 錯誤。原因在於 XMP fx:ConformanceLevel 欄位並不是讓您自己命名設定檔的自由文字欄位。Factur-X 為它定義了剛好五個標準值:MINIMUM、BASIC WL、BASIC、EN 16931 以及 EXTENDED。就 XMP 詮釋資料而言,一張法國特定的 B2B 發票仍然是一份 EXTENDED 設定檔文件。這張發票的法國特性並非透過發明第六個遵循值來表達。它是透過國家代碼 FR,以及 XML 內部的指南識別碼來表達,該識別碼必須帶有 urn:cen.eu:en16931:2017#conformant# 前綴,以標記出符合 EN 16931 標準的 CIUS。傳入標準的 EXTENDED 值,並以 FR 作為國家代碼以及正確的指南 URN,便使檔案符合規範

在函式庫 API 中,這是對 AddFacturXAssociatedFileFromString 的一次呼叫,並且遵循、國家和指南都已對齊。遵循層級引數帶有標準代碼,國家代碼引數帶有 FR,而指南 URN 則位於您傳入的 XML 位元組中

var
  FileID: Integer;
begin
  PDF.SetPDFAMode(5);            // PDF/A-3b
  PDF.NewDocument;
  // ... 繪製人類可讀的發票頁面 ...
  // ExtendedXML 帶有形式如下的 EN 16931 指南 URN
  //   urn:cen.eu:en16931:2017#conformant#urn:factur-x.eu:1p0:extended
  FileID := PDF.AddFacturXAssociatedFileFromString(
    ExtendedXML,
    'EXTENDED',          // 標準的 fx:ConformanceLevel,非內部標籤
    'factur-x.xml',
    'Factur-X EXTENDED invoice',
    'Alternative',       // /AFRelationship
    '1.0',
    'FR');               // 法國 B2B 由國家代碼標記,非由遵循層級標記
  if FileID = 0 then
    raise Exception.Create('Factur-X attachment rejected');
  PDF.SaveToFile('02_Factur-X-EXTENDED-FR_PDFA-3b.pdf');
end;

第二個原因是 ZUGFeRD 1.0 COMFORT 設定檔,這與詮釋資料毫無關係。ZUGFeRD 1.0 是根據 :1p0 XSD 進行驗證的,它在基數(cardinality)方面的要求比說明文字所暗示的還要嚴格。XSD 要求標頭結算總和 ram:SpecifiedTradeSettlementMonetarySummation 必須精確地各包含一次 ram:ChargeTotalAmountram:AllowanceTotalAmount。生成的 XML 遺漏了這兩者,因此 Mustang 回報說這些元素必須剛好出現一次。當綱要說明 minOccurs 為一的時候,這些就不是可選的。按照 XSD 序列順序,緊接在 ram:LineTotalAmount 之後發出這兩者,當沒有費用或折讓時,其值設為 0.00,這樣就能滿足綱要。一個零代表存在的元素;遺失元素則是違反綱要。在這兩個修正到位後,矩陣在 Mustang 上的通過率達到了十二分之十二,同時在 veraPDF 上也保持了十二分之十二

將無效翻轉為有效的 XRechnung 欄位

XRechnung 值得特別一提,因為它的德國 CIUS 增加了一些基礎 EN 16931 集合中沒有的業務規則,而且它們失敗的方式一眼看去就像是文件沒有任何問題一樣。其中有兩個與電子郵件地址有關。BT-34 是賣方的電子郵件地址,而 BT-49 是買方的電子郵件地址,它們是德國公部門入口網站用來遞送和確認發票的路由端點(routing endpoints)。基礎 EN 16931 模型將它們視為可選項。XRechnung 則不然。只要省略其中一個,發票即使格式良好且綱要有效,也會被拒絕

第三個是 BR-DE-6 規則,它要求必須提供賣方的聯絡電話號碼。這是一種開發人員會因為感覺像是呈現層(presentation)而不是資料層而放棄的欄位,它的缺失會產生一個指向賣方聯絡人群組,而不是任何明顯缺失事物的驗證失敗。提供 BT-34、BT-49 以及賣方的電話號碼,是讓 XRechnung 檔案在 Mustang 下從無效變為有效的關鍵,而這一切都不會改變 veraPDF 所看到的任何東西,因為這三個元素都存在於 XML 中

將函式庫輸出連線到驗證器

測試載具背後的架構觀點可推廣至任何商業系統。PDF 函式庫寫入合規的容器並內嵌 XML。它不會,也不應該,試圖成為 EN 16931 業務規則的權威。函式庫中的 ValidateFacturXInvoice 會檢查容器的一致性,確保目錄(catalog)的 /AF 陣列、內嵌檔案名稱樹、XMP 的 DocumentFileName、設定檔、指南以及 /AFRelationship 全部都一致,但它不會驗證稅碼或核對金額。正確的分工是讓商業系統提取 XML,並將其交給專用的發票驗證器,就像測試載具將它交給 Mustang 一樣

讀回檔案會告訴您實際寫入了什麼。DetectFacturXInvoice 會回報是否辨識出發票,而 GetFacturXInvoiceInfo 則透過標籤讀取詮釋資料欄位:標籤 1 是內嵌檔名,標籤 2 是 XMP DocumentFileName,標籤 5 是遵循層級,標籤 6 是指南識別碼,而標籤 7 是 /AFRelationship。確認您讀回來的遵循層級是標準代碼而不是內部標籤,是在檔案離開您的建置之前,捕捉到 EXTENDED 錯誤最划算的方法

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 指南 ID
    Writeln('Profile:   ', Profile);
    Writeln('Guideline: ', Guideline);
    // 將原始 XML 交給專用的 EN 16931 / Mustang 驗證器。
    Result := PDF.ExtractFacturXXMLToString;
  end;
end;

ExtractFacturXXMLToString 會將原始 XML 位元組作為一個 AnsiString 回傳,準備好寫入檔案或串流到驗證器程序中。在測試載具中,目標是 Mustang,透過其命令列的 jar 檔進行呼叫,同時 veraPDF 也會在對同一個檔案進行的同一次掃描中執行。這些連線的程式碼很少:一個主控台產生器 EInvoiceValidation.dpr,使用範例中共享的發票模型寫入十二個檔案,而一個指令碼 run-validation.ps1,驅動兩個驗證器掃描輸出目錄,並印出通過與失敗的表格。這種分兩步的形式(先使用函式庫產生,再使用外部驗證器進行驗證),正是持續整合(CI)作業在每次更改發票生成時應該執行的動作,因為要知道一個檔案是否滿足這兩層要求的唯一方法,就是詢問這兩個工具

如果您的管線還必須在簽章之前認證容器,這項工作的預檢部分已在我們關於 Delphi 中 PDF/A 與 PDF/UA 預檢的導覽中涵蓋,而更廣泛的「先認證後簽章」流程則在合規與簽章工作台中有詳細描述。兩者皆建立在相同的生成路徑上,作為適用於 Delphi 和 C++Builder 的 Delphi PDF 函式庫的一部分隨附發佈,並與此處使用的 PDF/A、關聯檔案與詮釋資料 API 齊頭並進