技術文章

使用 HotPDF 在 Delphi 中處理 ZUGFeRD 與 Factur-X 電子發票

HotPDF 可在 Delphi 與 C++Builder 中生成、偵測與驗證 ZUGFeRD 與 Factur-X 電子發票;AddFacturXAssociatedFile 將 EN 16931 發票 XML 內嵌到 PDF/A-3 容器中,DetectFacturXInvoice 識別傳入的混合發票,而 HPDFValidateEInvoice 則針對內嵌的 XML 執行官方的 EN 16931 Schematron 業務規則;本篇文章將引導您瞭解這三個步驟,並坦白說明驗證引擎在何處停止運作

這項功能集背後的壓力來自法規,而非技術;德國和法國正在逐步推行國內 B2B 交易的強制性結構化電子發票,且這兩項指令都收斂至相同的混合模式:您的 ERP 已經產生的 PDF 發票中,必須攜帶一個機器可讀的 XML 雙胞胎;現今發送純 PDF 發票的 Delphi 會計或 ERP 應用程式,在開始發送 Factur-X 或 ZUGFeRD 方面有著迫切的截止期限,而這兩個名稱之間的差異比行銷宣傳所暗示的要小——Factur-X 與 ZUGFeRD 是在兩個標籤下發布的同一個法德標準

是什麼讓 PDF 成為有效的 Factur-X 發票?

一個有效的 Factur-X 發票是一個 PDF/A-3 檔案(ISO 19005-3),它嵌入了正好一個結構化發票 XML 做為關聯檔案、從檔案目錄連結它,並在 XMP 擴充中介資料中宣告它;三個層級必須一致;第一,容器必須符合 PDF/A-3 規範,這是一種與 PDF/A-1 和 PDF/A-2 不同的封存設定檔,它允許嵌入任何類型的檔案;第二,在標準設定檔中名為 factur-x.xml 或在德國 XRechnung 變體中名為 xrechnung.xml 的發票 XML,必須註冊在目錄 /AF 陣列與 /Names /EmbeddedFiles 樹中,並帶有說明附件性質的 AFRelationship 項目:Alternative 表示該 XML 是可見發票的等效表示,這是大多數設定檔要求的語義;第三,Factur-X XMP 擴充結構描述必須指名內嵌檔案、其相容性等級和檔案類型,以便接收系統在完全不解析 XML 的情況下對發票進行分類

為什麼 ZUGFeRD 2.5 不需要新的容器

ZUGFeRD 2.5 在 PDF 容器層級上沒有改變任何內容:指導方針 URN、XMP 結構描述和版本權杖皆與 ZUGFeRD 2.3 / Factur-X 1.0 相同;這讓大多數實作者感到驚訝,因此值得引用來源;Factur-X 1.09 規格說明,PDF/A 擴充結構描述的 URI 中的版本號與 XML 資料規格的版本號無關,且 Factur-X 發票執行個體的版本控制在 XMP fx:Versionfactur-x.xml 的 BT-24 中同樣保持為 1.0;官方的 ZUGFeRD 2.5 範例語料庫證實了這一點:每個範例都將 factur-x.xml 嵌入在 urn:factur-x.eu:1p0 指導方針 URN 下,其 fx:Version1.0;2.5 實際新增的內容存在於 XML 資料模型內部——例如子發票行和批次識別碼等新元素——這是您的應用程式生成的業務層,而不是 PDF 函式庫管理的容器層

HotPDF 直接將此發現進行編碼;AddFacturXAssociatedFile 接受 Version 引數,並在寫入 XMP 之前透過 HPDFFacturXNormalizeVersion 對其進行標準化,因此傳入 2p5 別名的呼叫者仍然會發出符合規格的權杖,而不是發明一個沒有驗證器會接受的 2p5 XMP版本;偵測協助工具在讀取時將 2p5 視為等效,因此雙向轉換保持對稱;如果某個函式庫或工具告訴您它寫入了一個特殊的 ZUGFeRD 2.5 容器,規格本身說明了沒有這樣的東西

從 Delphi 內嵌發票 XML

THotPDF.AddFacturXAssociatedFile 透過單次呼叫即可將一般的 PDF/A-3 生成執行轉變為 Factur-X 發票:它將 XML 位元組嵌入為關聯檔案,在目錄 /AF 陣列與 /Names /EmbeddedFiles 樹中註冊 factur-x.xml,並發出相對應的 Factur-X XMP 擴充中介資料;您使用一般的 HotPDF 畫布 API 繪製人類可讀的發票頁面;容器工作只是 BeginDocEndDoc 之間的一行程式碼

var
  Pdf: THotPDF;
  InvoiceXML, UBLXML: TBytes;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.FileName := 'invoice-2026-0042.pdf';
    Pdf.PDFACompliance := '3B';            // PDF/A-3b container
    Pdf.BeginDoc;
    // embed the EN 16931 CII invoice XML as factur-x.xml
    InvoiceXML := BuildInvoiceXML;         // produced by your business layer
    Pdf.AddFacturXAssociatedFile(InvoiceXML, 'EN 16931');
    // optional: attach a UBL view as a supplementary representation
    UBLXML := BuildUBLXML;
    Pdf.AddUBLSupplementaryFile(UBLXML);   // factur-xubl.xml, Alternative
    // ... draw the visible invoice page here ...
    Pdf.EndDoc;
  finally
    Pdf.Free;
  end;
end;

預設值遵循規格:檔案名稱 factur-x.xml、關係 Alternative、版本 1.0AddUBLSupplementaryFile 實作了 Factur-X 1.09 規格的第 6.4 節,該節允許同一個發票的 UBL 表示形式做為第二個附件(名為 factur-xubl.xml、MIME 類型為 application/xml 且關係為 Alternative)一同攜帶;這裡的順序很重要,且 HotPDF 強制執行規格所暗示的限制:UBL 檔案是一個補充檢視,絕不是主發票,因此它必須在 AddFacturXAssociatedFile 註冊 CII 主檔案之後新增;請注意,繪製的頁面與內嵌的 XML 必須說明相同的發票——顯示一個總額但編碼另一個總額的 PDF,正是混合發票設計旨在防止的失敗模式;此工作流程的 PDF/A-3 部分(包括 OutputIntent 和字型嵌入)在 Delphi 中 PDF/A、PDF/X 與 PDF/UA 驗證的隨附文章 中有更深入的介紹

如何偵測傳入 PDF 中的 Factur-X 發票?

THotPDF.DetectFacturXInvoice 回應了法規的接收端要求:載入任何 PDF,並透過單次呼叫了解它是否為混合發票、它宣告了哪個設定檔,以及它攜帶了哪些附件;該函式傳回一個 THPDFFacturXInvoiceInfo 紀錄,其中包含內嵌 XML 檔案名稱、XMP 宣告的檔案名稱與類型、版本權杖、相容性等級、指導方針 URN,以及 AFRelationship 值;另外兩個欄位報告補充檢視:當存在 factur-xubl.xml 附件時會填入 UBLFileName,而當有 EDIFACT 檢視端攜帶時會填入 EDIFACTFileName——兩者都不會影響偵測判斷,因為沒有主要 CII 發票,補充表示形式就無法存在

偵測會讀取與符合標準之驗證器相同的訊號——XMP 擴充結構描述與關聯檔案登錄檔——而不是僅從檔案名稱進行猜測;同一個已載入檔案物件也公開了 XMP 與 Info 字典中介資料以供檢查或修正,該工作流程在 在已載入 PDF 檔案中編輯中介資料的文章 中有詳細說明

業務規則或結構描述:EN 16931 驗證代表什麼意思?

HPDFEInvoiceValidator 單元中的獨立函式 HPDFValidateEInvoice 依序執行兩個驗證層:首先是透過 ValidateFacturXInvoice 進行容器結構檢查,然後針對擷取出的發票 XML 執行 EN 16931 Schematron 業務規則;HotPDF 在 Lib/resources/Schematron/ 下隨附了五個官方 Factur-X 1.09 規則檔案——MINIMUM、BASIC WL、BASIC、EN 16931 和 EXTENDED,且 HPDFSchematronFileForProfile 會挑選與偵測到的相容性等級相符的檔案,因此 EN 16931 發票會自動針對 Factur-X_1.09_EN16931.sch 進行檢查;該函式僅在容器有效且沒有觸發 any 業務規則時傳回 True

uses HPDFEInvoiceValidator;

var
  Reader: THotPDF;
  Report: THPDFEInvoiceValidationReport;
  I: Integer;
begin
  Reader := THotPDF.Create(nil);
  try
    Reader.LoadFromFile('incoming-invoice.pdf');
    if HPDFValidateEInvoice(Reader, SchematronDir, Report) then
      Writeln('Container and business rules: OK')
    else
      Writeln('Validation found issues.');
    if Report.BusinessRulesEvaluated then
    begin
      Writeln(Report.BusinessSummary.Evaluated, ' rules evaluated, ',
        Report.BusinessSummary.Skipped, ' skipped (XPath 2.0)');
      for I := 0 to High(Report.BusinessRules) do
        if (not Report.BusinessRules[I].Skipped) and
           (Report.BusinessRules[I].Severity = stsError) then
          Writeln(string(Report.BusinessRules[I].RuleID), ': ',
            string(Report.BusinessRules[I].Message));
    end;
  finally
    Reader.Free;
  end;
end;

BusinessRules 中的每個項目都攜帶從斷言文字解析出的規則識別碼——BR-45BR-CO-17 以及其餘的 EN 16931 命名架構——加上規則內容 XPath、測試運算式,以及鏡像 Schematron 旗標屬性的嚴重性等級;這使報告具有可操作性:您得到的是不一致的確切業務條款,而不是單純的通過/失敗,這正是當客戶詢問發票為何被退回時,您的支援團隊所需要的資訊;已經為封存 PDF 生成結構化相容性報告的團隊,可以將這些結果併入與 自動化 PDF 預檢報告文章 中描述的相同管線中

坦白說明驗證器停止作用之處

Schematron 引擎建置於 MSXML 之上,它求值 XPath 1.0;EN 16931 規則檔案宣告了 queryBinding="xslt2" 並攜帶 424 個斷言,其中大多數僅使用 XPath 1.0 函式(例如 string-lengthsubstring-after)——這些是原生執行的;少數斷言依賴 XPath 2.0 結構(例如 xs:decimal 轉型或 exists());HotPDF 會預先掃描每個測試運算式以尋找已知的 XPath 2.0 權杖,並將這些規則標記為 Skipped(嚴重性設為資訊),而不是嘗試執行會默默出錯的求值;摘要紀錄會分別報告已評估和已跳過的計數,因此您的日誌總是能準確說明執行了多少規則集;在無法使用 MSXML 繫結的 Delphi 7 上,該單元會編譯一個存根 (stub),報告引擎不可用,且不會干擾容器判斷;完整的引擎需要 Delphi XE2 或更新版本

第二個界限甚至更為重要;HPDFValidateEInvoice 的綠色結果表示容器結構符合規範且未觸發任何已評估的業務規則——它不是稅務合規的背書;國家擴充規則、接收者專屬需求以及圍繞發票內容的法律義務,都高於 EN 16931 核心,且在任何函式庫之外;請將驗證器視為防止結構損壞的發票進入寄件匣並在收件匣中標記不一致發票的閘門,當驗證需要時,在您的發布管線中執行完整的 XPath 2.0 驗證器,並讓您的稅務顧問負責處理合規問題;此處顯示的內嵌、偵測與驗證 API 是 Delphi 與 C++Builder 標準 HotPDF 元件 的一部分,並附帶一個涵蓋所有七個 ZUGFeRD 與 Factur-X 設定檔的完整電子發票範例