技術文章

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

Factur-X 或 ZUGFeRD 發票是共用同一個檔名的兩份文件。外層文件是 PDF/A-3 容器,封存讀取器必須在未來十年內接受它;內層文件是 XML 發票,買方的會計系統必須依照 EN 16931 加以解析。將壞掉的發票送進正式環境的常見錯誤,就是誤以為只要第一層正確,第二層就能免費搭便車。事實並非如此。一份檔案可以是完美無缺的 PDF/A-3,卻包含任何稅務機關都不會接受的 XML;也可以包含教科書等級的 EN 16931 XML,但容器卻無法通過封存驗證。兩個層次由兩套彼此毫不相知的工具分別驗證,真正的處理管線必須同時滿足這兩套要求

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

veraPDF 是 PDF/A 的參考實作。將發票指向它,它只回答一個問題:這份檔案是否符合 PDF/A-3 規範。它檢查的是 ISO 19005-3 所關心的事項:每種字型是否都已嵌入、是否有 OutputIntent、XMP 中繼資料是否宣告了正確的部分與符合層級。對於電子發票,它也會檢查 PDF/A-3 所要求的關聯檔案管線結構,因為 XML 是以嵌入檔案的形式附帶其中,並具有 /AFRelationship 以及文件目錄 /AF 陣列中的項目。veraPDF 不會評論發票金額是否加總正確,因為那不在它的職責範圍內

Mustang 是 Mustangproject 的開源驗證器,它問的是正交的問題:嵌入的 XML 是否是有效的發票。它根據所宣告的設定檔對 XML 執行結構描述驗證,然後套用 EN 16931 商業規則以及疊加其上的各國專屬規則集,包括 XRechnung 的 CIUS。它會檢查當合計金額要求時賣方增值稅識別碼是否存在、折讓與費用金額是否與文件合計相符、XML 中的設定檔 URN 是否與檔案所宣稱的一致。Mustang 不在乎周圍的 PDF 是否已嵌入字型,因為那是 veraPDF 的工作

這兩個工具都不是對方的超集。veraPDF 會讓結構完美的容器通過,即使其中包裹的是無意義的 XML;Mustang 會讓完美的 XML 通過,即使它被包裹在缺少 OutputIntent 的容器中。每個工具只能捕捉對方看不到的那類缺陷,這正是嚴謹的驗證流程需要同時執行兩者、並只在兩者都認可時才視為可發貨的全部原因

驗證矩陣

為了證明函式庫產生的檔案能通過這兩道關卡,測試工具建立了一個矩陣。六種發票設定檔涵蓋了歐洲處理管線在實務中所遇到的範圍: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 範例所採用的相同程式碼路徑以無頭方式建立,因此受測的成品並非為測試而手工調整

產生器寫出全部十二個檔案,然後由腳本將每個檔案分別送給兩個驗證器。在第一次完整執行時,veraPDF 全部通過。容器管線結構在所有情況下都是正確的:關聯檔案已登錄、XMP 符合性已宣告、輸出意圖已就位。Mustang 通過了八個。四個發票在結構上是有效的 PDF/A-3 檔案,但其中包含的 XML 卻被商業規則驗證器拒絕,這正是兩工具方法存在的意義所在 - - 若測試工具只信任 veraPDF,這四個檔案看起來就已完成了

彌補差距的兩個修正

Mustang 的四個失敗案例源於兩個截然不同的原因,在您自行產生這些設定檔之前,每個原因的修正細節都值得了解

第一個是 Factur-X EXTENDED 法國 B2B 設定檔。原始產生器將一個內部標籤作為符合層級、將一個內部 URN 作為指導方針傳入,Mustang 因此回報了「符合值無效」錯誤,隨後是「不支援的設定檔類型」錯誤。原因在於 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;
  // ... 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;

第二個原因是 ZUGFeRD 1.0 COMFORT 設定檔,與中繼資料毫無關係。ZUGFeRD 1.0 依照 :1p0 XSD 進行驗證,該 XSD 在基數方面比摘要文件的說明更為嚴格。XSD 要求標頭結算彙總 ram:SpecifiedTradeSettlementMonetarySummation 中必須各恰好包含一個 ram:ChargeTotalAmountram:AllowanceTotalAmount。產生的 XML 省略了這兩個元素,因此 Mustang 回報這些元素必須恰好出現一次。當結構描述指定 minOccurs 為 1 時,這些元素並非選用項目。在 XSD 序列順序中、緊接於 ram:LineTotalAmount 之後,分別發出這兩個值,若無費用或折讓則以 0.00 表示,即可滿足結構描述。值為零是存在的元素;缺失的元素是結構描述違規。完成這兩個修正後,矩陣在 Mustang 上達到十二個中的十二個通過,同時在 veraPDF 上也維持十二個中的十二個通過

讓 XRechnung 從無效變為有效的欄位

XRechnung 值得單獨說明,因為它的德國 CIUS 添加了基本 EN 16931 規則集中沒有的商業規則,而這些規則的失敗方式乍看之下像是文件毫無問題。其中兩個涉及電子地址。BT-34 是賣方電子地址,BT-49 是買方電子地址,這是德國公共部門入口用來傳遞和確認發票的路由端點。基本 EN 16931 模型將它們視為選用項目,XRechnung 則不然。省略其中任一個,發票就會是格式正確、結構描述有效,卻被拒絕的狀態

第三個是規則 BR-DE-6,要求賣方聯絡電話號碼必須存在。這是開發人員容易省略的欄位,因為感覺像是展示資訊而非資料,其缺失會產生指向賣方聯絡群組的驗證失敗,而不是指向任何明顯缺少的內容。提供 BT-34、BT-49 以及賣方電話號碼,才能讓 XRechnung 檔案在 Mustang 下從無效變為有效,而且這三項都不會改變 veraPDF 所看到的任何內容,因為它們全都位於 XML 中

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

測試工具背後的架構要點可推廣至任何商業系統。PDF 函式庫負責寫入符合規範的容器並嵌入 XML,它不應該、也不會試圖成為 EN 16931 商業規則的權威。函式庫中的 ValidateFacturXInvoice 負責檢查容器一致性 - - 目錄 /AF 陣列、嵌入檔案名稱樹、XMP DocumentFileName、設定檔、指導方針以及 /AFRelationship 是否全部一致 - - 但不驗證稅碼或核對金額。正確的分工是讓商業系統擷取 XML 並交給專用發票驗證器,就像測試工具將 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 guideline ID
    Writeln('Profile:   ', Profile);
    Writeln('Guideline: ', Guideline);
    // Hand the raw XML to a dedicated EN 16931 / Mustang validator.
    Result := PDF.ExtractFacturXXMLToString;
  end;
end;

ExtractFacturXXMLToStringAnsiString 形式回傳原始 XML 位元組,可直接寫入檔案或串流至驗證器處理程序。在測試工具中,目標是透過命令列 jar 呼叫的 Mustang,並在同一次對同一個檔案的處理過程中同時執行 veraPDF。連接方式很簡單:主控台產生器 EInvoiceValidation.dpr 使用範例中的共用發票模型寫出十二個檔案,腳本 run-validation.ps1 對輸出目錄執行兩個驗證器並列印通過與失敗的表格。這個「使用函式庫產生、再用外部驗證器驗證」的兩步驟模式,就是持續整合工作在每次發票產生邏輯變更時都應執行的內容,因為唯一能確認檔案同時滿足兩個層次的方式,就是詢問兩個工具

如果您的處理管線在簽署之前也需要認證容器,這部分工作的預檢面向在我們的 Delphi PDF/A 與 PDF/UA 預檢逐步說明中有所介紹,更廣泛的認證後簽署流程則在合規性與簽署工作台中說明。兩者都建立在相同的產生路徑上,該路徑作為 Delphi PDF Library 的一部分,適用於 Delphi 和 C++Builder,並包含此處所使用的 PDF/A、關聯檔案和中繼資料 API