符合規範的電子發票並不是一份附加了 XML 檔案的 PDF。它是一份單一的 PDF/A-3 文件,以兩種形式承載同一份發票:一次是供人類閱讀的頁面,一次是以關聯檔案形式儲存在檔案內部、可由機器讀取的跨行業發票 XML。這兩種呈現形式描述的是同一份發票。這種雙重性質正是歐洲法規所要求的各格式家族的核心所在 - - 法國與德國採用 Factur-X、德語市場普遍採用 ZUGFeRD、德國公部門帳單採用 XRechnung。本文說明 PDFlibPas 如何在 Delphi 中組裝這種混合發票、標準在哪些地方留下了出錯的空間,以及為何目錄中的某個設定檔需要完全獨立的 XML 建構器
混合發票的本質
可見頁面與嵌入的 XML 服務於不同的讀者。批准付款的辦事員看的是渲染後的頁面;應付帳款系統則讀取 XML,將合計金額與稅項明細作為結構化欄位讀入,並在無需人工輸入的情況下完成記帳。XML 的語義內容由 EN 16931 管轄 - - 這個歐洲標準定義了發票資料模型:哪些欄位存在、它們的含義,以及哪些是必填項目。EN 16931 是語義模型,而非檔案格式。Factur-X、ZUGFeRD 2.x 和 XRechnung 都將該模型實現為 UN/CEFACT 跨行業發票文件,這是在傳輸過程中承載 EN 16931 欄位的語法格式
為使文件既可封存又能自我描述,容器採用 ISO 19005-3 所定義的 PDF/A-3。PDF/A-3 是允許嵌入任意檔案的符合層級,而這正是發票 XML 所需要的。PDF/A-2 禁止嵌入本身不是 PDF/A 的檔案,因此 Factur-X 發票不能是 PDF/A-2。選用 PDF/A-3 並非偏好問題,而是直接源自「在封存文件中嵌入非 PDF 資料」這一需求所帶來的必然要求
為何關係類型是 Alternative
嵌入位元組是容易的部分。ISO 32000 §7.11.4 定義了嵌入檔案串流 - - 用於存放原始 XML 及其參數的物件。使檔案成為有效關聯檔案的是 §14.13,它新增了關聯檔案的概念以及 /AFRelationship 鍵。該鍵說明嵌入資料與其所附加內容之間的關係,而 Factur-X 規定的值是 Alternative
這個選擇至關重要,因為其他值會對文件做出不實的宣告。Source 意味著 XML 是產生可見內容的來源素材 - - 頁面從中衍生出來的母本;Supplement 則意味著 XML 添加了頁面所未顯示的額外資訊。這兩者都不是 Factur-X 發票的本質。XML 與頁面是同一份發票的兩種等效表達方式,以兩種形式承載相同的法律內容。Alternative 正是表達這個意義的值:可見內容的等效替代呈現形式。驗證器若在 Factur-X 檔案上讀到任何其他關係類型,都會拒絕它 - - 因為關係類型是一個機器可讀的宣告,說明附件的用途
設定檔目錄
PDFlibPas 隨附的電子發票範例以六種設定檔驅動相同的產生路徑,這些設定檔在 InvoiceModel.pas 中以記錄陣列的形式定義。每個設定檔都包含寫入器所需的值:顯示名稱、嵌入檔案名稱、符合層級、/AFRelationship、版本、可選的國家代碼,以及 XML 在其文件上下文中宣告的 GuidelineID URN
這六種設定檔是:Factur-X EN16931、Factur-X BASIC、法國 Factur-X EXTENDED、XRechnung 3.0、ZUGFeRD 1.0 COMFORT 以及 ZUGFeRD 2.0 BASIC。GuidelineID 欄位精確地告知接收方應期待哪種設定檔,其值是特定的。Factur-X EN16931 宣告 urn:cen.eu:en16931:2017;XRechnung 3.0 宣告 urn:cen.eu:en16931:2017#compliant#urn:xeinkauf.de:kosit:xrechnung_3.0;ZUGFeRD 2.0 BASIC 宣告 urn:cen.eu:en16931:2017#compliant#urn:zugferd.de:2p0:basic。嵌入的檔案名稱也是合約的一部分:Factur-X 設定檔嵌入 factur-x.xml,XRechnung 嵌入 xrechnung.xml,ZUGFeRD 設定檔嵌入 ZUGFeRD-invoice.xml 或 zugferd-invoice.xml。接收方透過掃描附件名稱來尋找發票,因此檔案名稱並非裝飾性的
目錄中有一個細節值得仔細閱讀。大多數設定檔使用 Alternative 關係,但範例中 XRechnung 3.0 條目使用的是 Source。這兩種格式遵循不同的驗證器與慣例,範例從目錄中讀取每個設定檔的關係類型,而非將單一值寫死,這正是每個設定檔都有此欄位、而非使用常數的原因
ZUGFeRD 1.0 的陷阱
人們很容易假設每個設定檔都是 EN 16931 跨行業發票,只是在填入多少選用欄位方面略有不同。這對五個設定檔是成立的,但對 ZUGFeRD 1.0 COMFORT 不成立 - - 原因是結構性的,而非表面性的
現代設定檔發出的是命名空間版本為 :100 的 UN/CEFACT 跨行業發票,其根元素為 rsm:CrossIndustryInvoice。ZUGFeRD 1.0 早於該結構描述,它是 2014 年的 CrossIndustryDocument,命名空間版本為 :1p0,根元素為 rsm:CrossIndustryDocument。命名空間 URN 不同、根元素不同,整個元素樹也不同::1p0 結構描述將資料分組於 ApplicableSupplyChainTradeAgreement、ApplicableSupplyChainTradeDelivery 和 ApplicableSupplyChainTradeSettlement 之下,而 :100 則使用 ApplicableHeaderTradeAgreement、ApplicableHeaderTradeDelivery 和 ApplicableHeaderTradeSettlement。這兩者的命名足夠相似,容易造成誤導,卻又足夠不同,足以導致錯誤
設定檔名稱中的 COMFORT 描述的是資料的豐富程度 - - 一個具有完整明細項目、稅項明細和付款條件的自動化級設定檔 - - 而非承載它的結構描述。因此,您無法取用一份 :100 文件並重新標記為 ZUGFeRD 1.0。範例使用每個設定檔記錄上的旗標以及兩個獨立的建構器函式來處理這種情況,在產生任何 XML 之前先選擇正確的建構器
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;
這個拆分並非實作上的細節。將 :100 樹狀結構輸入 ZUGFeRD 1.0 接收方,會產生一份在根元素就無法通過結構描述驗證的文件,因此這兩個家族必須由知道自己在寫哪個家族的程式碼來建立
選擇 PDF/A-3 層級
PDF/A-3 有三個符合層級,PDFlibPas 透過 SetPDFAMode 來選擇。模式 5 是 PDF/A-3b,保證可靠的視覺再現;模式 6 是 PDF/A-3a,增加了層級 a 的標記結構與無障礙要求;模式 7 是 PDF/A-3u,要求所有文字都能對應至 Unicode。啟用模式同時也嵌入了函式庫內建的 sRGB 輸出意圖 - - 這是 PDF/A 要求的色彩特性,確保渲染後的色彩是有定義的,而非依賴裝置
大多數發票流程在 3b 下執行,這對於忠實的可見頁面加上嵌入的 XML 已經足夠。如果您需要明確的 ICC 設定檔而非內建的,可以在設定模式後呼叫 LoadOutputIntentProfile 來替換。範例以此方式載入儲存庫中的 sRGB 設定檔,並在檔案無法取得時退回到內建意圖,因此輸出意圖始終存在
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;
建立混合發票
容器設定完成後,其餘步驟依序分為三步:設定 PDF/A-3 模式、繪製供人類閱讀的頁面,然後以關聯檔案形式附加 XML。可見頁面是普通內容。唯一值得記住的限制是 PDF/A 禁止未嵌入的標準 14 字型,因此頁面必須嵌入真實的字型面,而非引用內建字型
附加操作只需一次呼叫。AddFacturXAssociatedFileFromString 接收原始 UTF-8 XML 位元組加上設定檔中繼資料,寫入嵌入檔案串流,將其登錄到 PDF/A-3 所要求的目錄 /AF 陣列中,套用 /AFRelationship,並產生將文件識別為 Factur-X、ZUGFeRD 或 XRechnung 的 XMP 電子發票中繼資料。它也會檢查 XML 的指導方針 ID 是否與您所要求的符合層級相符,因此您所建立的 XML 與您所命名的設定檔之間的不符,會被捕捉到而非默默地發出
// 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);
資料路徑中有一個微妙之處是編碼。嵌入的 XML 宣告 encoding="UTF-8",而此方法以 AnsiString 形式接收位元組,因此含有非 ASCII 字元的賣方或買方名稱必須以原始 UTF-8 八位元組的形式到達呼叫點。透過系統 ANSI 字碼頁進行普通轉型會損壞這些字元,並默默地產生 XML 與其自身宣告不符的發票。範例在傳遞位元組之前明確地編碼為 UTF-8,這是從 Unicode string 向任何以位元組為導向的 PDF API 傳遞資料的安全方式
對於附加非已知電子發票設定檔的 XML,AddPDFA3AssociatedFileFromString 是通用的對應方法。它接收檔案名稱、MIME 類型、描述、關係類型和位元組,並寫入普通的 PDF/A-3 關聯檔案,不含任何發票專屬中繼資料或指導方針檢查。補充資料請使用它;發票請使用 Factur-X 方法,以便讓設定檔中繼資料和指導方針比對由方法為您處理
文件產生後,後續問題是它是否通過 PDF/A 和無障礙驗證,以及是否可以在不破壞合規性的情況下進行簽署。這些內容在PDF/A 與 PDF/UA 預檢逐步說明以及合規性與簽署工作台中有所介紹。所有這些都作為 PDFlibPas Delphi PDF Library 的一部分提供,並附有電子發票路徑所建立的 PDF/A、標記和文件屬性 API