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 要求的 associated-file(关联文件)管道连接,因为 XML 是作为一个带 /AFRelationship 并在文档目录 /AF 数组中有一个条目的嵌入文件随同的。veraPDF 并不说明发票总额是否一致,因为那不在它的职责范围内
Mustang 是来自 Mustangproject 的开源验证器。它提出正交的问题:嵌入的 XML 是一张有效发票吗。它根据声明配置文件的 schema 运行 XML,然后应用 EN 16931 业务规则以及叠加在上面的特定国家规则集,其中包括 XRechnung 的 CIUS。它检查当总计金额需要卖方 VAT 标识符时是否提供了该标识符,折扣和费用金额是否与文档总计一致,XML 中的配置文件 URN 是否与文件声明的一致。Mustang 不关心周围的 PDF 是否嵌入了其字体,因为那是 veraPDF 的工作
两种工具都不是对方的超集。veraPDF 能够通过包裹着无意义 XML 的结构上完美的容器。Mustang 能够通过包裹在缺少 OutputIntent 的容器中的完美 XML。每种工具都能精确捕捉到另一种工具看不见的缺陷类别,这就是为什么一个严肃的验证工具台要同时运行两者,并且只有在两者都同意时才将文件视为可发布的全部原因
验证矩阵
为了证明库生成的文件能够在两个关卡都存活,验证工具台构建了一个矩阵。六种发票配置文件涵盖了欧洲流水线在实践中遇到的范围:Factur-X EN 16931、Factur-X BASIC、Factur-X EXTENDED France B2B 变体、XRechnung 3.0、ZUGFeRD 1.0 COMFORT 和 ZUGFeRD 2.0 BASIC。每个配置文件都针对两个 PDF/A 子一致性级别(3b 和 3u)生成,因为 B 级别和 U 级别的要求在 Unicode 映射上存在分歧,通过一个级别的文件可能会在另一个级别上失败。六种配置文件乘以两个级别是十二个文件,每一个都是由与 GUI 示例发布的相同代码路径以无头方式生成的,因此被测组件不是为了测试而手动调整的
生成器写出所有十二个文件,一个脚本将每一个文件都喂给这两个验证器。在第一次完整运行时,veraPDF 通过了所有十二个。容器管道连接全面正确:注册了关联文件,声明了 XMP 一致性,OutputIntent 已到位。Mustang 通过了八个。四张发票是结构上有效的 PDF/A-3 文件,携带着业务规则验证器拒绝的 XML,这正是双工具方法要暴露的分歧。如果工具台只信任 veraPDF,那四个文件看起来就是完成的
缩小差距的两个修复
四个 Mustang 的失败来自两个不同的原因,在你亲自生成这些配置文件之前,了解每个修复的细节是值得的
第一个是 Factur-X EXTENDED France B2B 配置文件。原始生成器传递了一个内部标签作为一致性级别,并传递了一个内部 URN 作为 guideline(准则),Mustang 拒绝了该文件,先是一个无效一致性值错误,紧接着是一个不支持的配置文件类型错误。原因是 XMP fx:ConformanceLevel 字段并不是一个供你自己命名配置文件的自由文本槽。Factur-X 为它准确定义了五个标准值:MINIMUM、BASIC WL、BASIC、EN 16931 和 EXTENDED。就 XMP 元数据而言,一张法国特定的 B2B 发票仍然是一个 EXTENDED 配置文件的文档。发票的法国特性不是通过发明第六个一致性值来表达的。它是通过国家代码 FR 以及 XML 内部的 guideline 标识符来表达的,该标识符必须携带 urn:cen.eu:en16931:2017#conformant# 前缀,这标志着符合 EN 16931 的 CIUS。传递具有标准 EXTENDED 值、FR 作为国家代码和正确 guideline URN 的文件便使其符合规范
在库 API 中,那是对 AddFacturXAssociatedFileFromString 的一次调用,并带有一致性、国家和 guideline。一致性级别参数携带标准令牌,国家代码参数携带 FR,guideline 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 在基数(cardinality)上比散文摘要中暗示的更严格。XSD 要求头部结算汇总 ram:SpecifiedTradeSettlementMonetarySummation 必须各包含 ram:ChargeTotalAmount 和 ram:AllowanceTotalAmount 恰好一次。生成的 XML 省略了两者,因此 Mustang 报告说这些元素必须恰好出现一次。当 schema 说 minOccurs 是一时,这些不是可选的。按照 XSD 顺序,紧跟在 ram:LineTotalAmount 之后,在没有费用或折扣时发出值为 0.00 的两者,满足了 schema 的要求。零是一个存在的元素;缺失一个元素则是违反 schema。随着这两个修复就位,该矩阵在 Mustang 上达到了十二个通过,同时在 veraPDF 上保持着十二个通过
将无效变为有效的 XRechnung 字段
XRechnung 值得特别注意,因为其德国 CIUS 增加了基本 EN 16931 集合中所没有的业务规则,并且它们的失败方式让你乍一看觉得文档什么问题都没有。其中两个涉及电子地址。BT-34 是卖方的电子地址,BT-49 是买方的电子地址,也就是德国公共部门门户网站用于传递和确认发票的路由端点。基本 EN 16931 模型将它们视为可选。XRechnung 不是这样。忽略其中任何一个,发票即使是结构良好的、schema 有效的,也会被拒绝
第三个是规则 BR-DE-6,它要求必须存在卖方联系电话号码。这种字段通常被开发者丢弃,因为它感觉像是一种表现形式而不是数据,其缺失会产生一个验证失败,该失败指向卖方联系人组而不是任何明显缺失的东西。提供 BT-34、BT-49 和卖方电话号码才能使 XRechnung 文件在 Mustang 下从无效变为有效,而且这所有都不改变 veraPDF 所看到的任何东西,因为所有这三个都存在于 XML 中
将库输出连接到验证器
工具台背后的架构重点可以推广到任何业务系统。PDF 库编写一个符合规范的容器并嵌入 XML。它没有,也不应该,试图成为 EN 16931 业务规则权威。库中的 ValidateFacturXInvoice 检查容器的一致性,即目录 /AF 数组、嵌入文件名称树、XMP DocumentFileName、配置文件、guideline 以及 /AFRelationship 都一致,但它不验证税法代码或核对金额。正确的分工是由业务系统提取 XML 并将其交给专用的发票验证器,就像工具台将其交给 Mustang 一样
回读文件告诉你实际写入了什么。DetectFacturXInvoice 报告是否识别出发票,而 GetFacturXInvoiceInfo 按标签读取元数据字段:标签 1 是嵌入的文件名,标签 2 是 XMP DocumentFileName,标签 5 是一致性级别,标签 6 是 guideline 标识符,标签 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;
ExtractFacturXXMLToString 将原始 XML 字节作为 AnsiString 返回,准备好写入文件或流入验证器进程。在测试工具台中,该目标是 Mustang,通过其命令行 jar 调用,而 veraPDF 在同一次遍历同一文件时运行。管道连接很小:控制台生成器 EInvoiceValidation.dpr 使用示例中的共享发票模型写入十二个文件,而脚本 run-validation.ps1 驱动这两个验证器遍历输出目录并打印出通过和失败表。这种两步形式(由库生成并由外部验证器验证),是持续集成作业应在发票生成每次更改时运行的形式,因为确切知道文件满足两层要求的唯一方法就是询问这两个工具
如果你的流水线还必须在签名之前证明容器,这项工作的印前检查(preflight)方面在我们的 Delphi 中的 PDF/A 和 PDF/UA 印前检查演练中有所涉及,更广泛的先认证后签名的流程则在兼容性与签名工作台中进行了描述。两者都建立在相同的生成路径之上,该路径作为针对 Delphi 和 C++Builder 的 Delphi PDF Library 的一部分发布,与此处使用的 PDF/A、关联文件和元数据 API 在一起