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 不同,它允许嵌入任何类型的文件);其次,发票 XML(标准规范命名为 factur-x.xml,或德国 XRechnung 变体命名为 xrechnung.xml)必须在 Catalog 的 /AF 数组和 /Names /EmbeddedFiles 树中注册,并带有指明该附件是什么的 AFRelationship 条目: Alternative 意味着 XML 是可见发票的等效表示,这是大多数规范所要求的语义;第三,Factur-X XMP 扩展 Schema 必须命名嵌入的文件、其符合性级别和文档类型,以便接收系统可以在根本不解析 XML 的情况下对发票进行分类
XML 负载本身遵循 EN 16931 语义模型(这是定义核心发票的欧洲标准):买方、卖方、行项目、税收明细、应付金额,每个都带有业务术语编号(BT-1 为发票编号,BT-5 为货币,等等);因此,EN 16931 验证发生在两个截然不同的层面上;XML Schema 验证检查文档是否为具有正确元素名称的、格式良好的 CII;Schematron 业务规则检查内容是否连贯 —— 即 BR-CO-15 是否成立,发票总额是否确实等于行净额之和加上增值税,对于声明的规范是否存在强制性条款;文件可以通过 Schema 验证但仍是一份无效的发票,这正是业务规则层存在的原因
为什么 ZUGFeRD 2.5 不需要新的容器
ZUGFeRD 2.5 在 PDF 容器级别没有做任何改变:指南 URN、XMP Schema 和版本 Token 与 ZUGFeRD 2.3 / Factur-X 1.0 相同;这让大多数实现者感到惊讶,因此值得引用一下来源;Factur-X 1.09 规范指出,PDF/A 扩展 Schema 的 URI 中的版本号与 XML 数据规范的版本号无关,并且 Factur-X 发票实例的版本控制仍保持为 1.0 —— 在 XMP fx:Version 以及 factur-x.xml 的 BT-24 中都是如此;官方的 ZUGFeRD 2.5 样本库证实了这一点:每个样本都将 factur-x.xml 嵌入在 urn:factur-x.eu:1p0 指南 URN 下,且 fx:Version 为 1.0;2.5 实际增加的内容活在 XML 数据模型内部 —— 新增的元素(例如子发票行和批次标识符)—— 这是您的应用程序生成的业务层,而不是 PDF 库所管理的容器层
HotPDF 直接对这一发现进行了编码;AddFacturXAssociatedFile 接受 Version 参数,并在写入 XMP 前通过 HPDFFacturXNormalizeVersion 对其进行规范化,因此传递 2p5 别名的调用者仍会发出符合规范的 Token,而不是发明一个没有验证器会接受的 2p5 XMP 版本;检测辅助程序在读取时将 2p5 视为等同,从而保持往返处理的对称;如果有库或工具告诉您它写入了特殊的 ZUGFeRD 2.5 容器,规范本身会说明没有这种东西
从 Delphi 嵌入发票 XML
THotPDF.AddFacturXAssociatedFile 通过一次调用即可将普通的 PDF/A-3 生成运行转为 Factur-X 发票:它将 XML 字节嵌入为关联文件,在 Catalog 的 /AF 数组和 /Names /EmbeddedFiles 树中注册 factur-x.xml,并发出匹配的 Factur-X XMP 扩展元数据;您使用普通的 HotPDF 画布 API 绘制人类可读的发票页面,容器工作只是 BeginDoc 和 EndDoc 之间的一行代码:
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.0;AddUBLSupplementaryFile 实现了 Factur-X 1.09 规范的第 6.4 节,该节允许相同发票的 UBL 表示作为第二个名为 factur-xubl.xml 的附件(带有 application/xml 的 MIME 类型 and 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 声明的文档文件名和类型、版本 Token、符合性级别、指南 URN 和 AFRelationship 值;另外两个字段报告了补充视图:当存在 factur-xubl.xml 附件时填充 UBLFileName,当附带 EDIFACT 视图时填充 EDIFACTFileName —— 两者都不影响检测结论,因为补充表示在没有主要 CII 发票的情况下是不能存在的
var
Reader: THotPDF;
Info: THPDFFacturXInvoiceInfo;
begin
Reader := THotPDF.Create(nil);
try
if Reader.LoadFromFile('incoming-invoice.pdf') <= 0 then
raise Exception.Create('No pages loaded.');
if Reader.DetectFacturXInvoice(Info) then
begin
Writeln('Invoice XML: ', string(Info.FileName));
Writeln('Conformance: ', string(Info.ConformanceLevel));
Writeln('Guideline URN: ', string(Info.GuidelineID));
Writeln('Relationship: ', string(Info.AFRelationship));
if Info.UBLFileName <> '' then
Writeln('UBL view: ', string(Info.UBLFileName));
end
else
Writeln('Not a Factur-X / ZUGFeRD hybrid invoice.');
finally
Reader.Free;
end;
end;
检测读取符合规范的验证器所读取的相同信号 —— XMP 扩展 Schema 和关联文件注册表 —— 而不是仅靠文件名进行猜测;相同的加载文档对象还暴露了 XMP 和 Info 字典元数据以供检查或修正,该工作流在关于编辑加载的 PDF 文档中元数据的文章中进行了介绍
业务规则还是 Schema: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 进行检查;该函数仅在容器有效且没有触发任何业务规则时返回 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;
验证器在何处止步(坦诚而言)
Schematron 引擎建立在 MSXML 之上,MSXML 评估的是 XPath 1.0;EN 16931 规则文件声明了 queryBinding="xslt2" 并携带了 424 个断言,其中大多数仅使用 XPath 1.0 函数(例如 string-length 和 substring-after) —— 这些都是原生运行的;少数依赖于 XPath 2.0 构造,例如 xs:decimal 转换或 exists();HotPDF 预先扫描每个测试表达式中已知的 XPath 2.0 Token,并将这些规则标记为 Skipped(严重性为 info),而不是尝试进行可能会静默哑火的评估;摘要记录分别报告了已评估和已跳过的计数,因此您的日志始终准确说明了运行了多少规则集;在 MSXML 绑定不可用的 Delphi 7 上,该单元编译了一个存根,它报告引擎不可用,而不扰乱容器结论;完整的引擎需要 Delphi XE2 或更高版本
第二个边界更为重要;来自 HPDFValidateEInvoice 的绿色(通过)结果意味着容器结构符合且没有触发评估的业务规则 —— 这并不是税务合规性背后的背书;国家扩展规则、特定接收者的要求以及围绕发票内容的法律义务都位于 EN 16931 核心之上,并处于任何库之外;请将验证器视为把结构损坏的发票挡在发件箱之外、并在收件箱中标记不一致发票的关卡,在认证有需求时在您的发布流水线中运行完整的 XPath 2.0 验证器,并让您的税务顾问来解答合规性问题;此处显示的嵌入、检测和验证 API 都是适用于 Delphi 和 C++Builder 的标准 HotPDF Component 的一部分,并附带了涵盖所有七个 ZUGFeRD 和 Factur-X 规范的完整电子发票示例