기술 문서

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 인보이스는 구조화 인보이스 XML을 연관 파일로 정확히 하나 임베드하고, 그것을 문서 카탈로그에서 연결하고, XMP 확장 메타데이터에 선언한 PDF/A-3 파일(ISO 19005-3)입니다. 세 계층이 서로 맞아야 합니다. 첫째, 컨테이너가 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을 전혀 파싱하지 않고도 인보이스를 분류할 수 있습니다

유효한 Factur-X 인보이스의 세 계층인 PDF/A-3 컨테이너, 임베드된 factur-x.xml 연관 파일, XMP 확장 메타데이터를 보여 주는 HotPDF 다이어그램
컨테이너, 첨부 등록부, XMP 메타데이터가 서로 맞아떨어져야 그 PDF가 Factur-X 인보이스로 인정됩니다

XML 페이로드 자체는 EN 16931 의미 모델을 따릅니다. 핵심 인보이스를 정의하는 유럽 표준으로 구매자, 판매자, 품목, 세액 내역, 지급액을 각각 업무 용어 번호와 함께 규정합니다(인보이스 번호는 BT-1, 통화는 BT-5 하는 식입니다). 그래서 EN 16931 검증은 뚜렷이 구분되는 두 계층에서 일어납니다. XML 스키마 검증은 문서가 올바른 요소 이름을 지닌 형식에 맞는 CII인지 확인합니다. Schematron 업무 규칙은 내용이 일관되는지를 확인합니다 — BR-CO-15가 성립하는지, 인보이스 총액이 실제로 품목 순액 합계에 부가세를 더한 값과 같은지, 선언된 프로파일에 필수인 용어가 존재하는지 같은 것들입니다. 어떤 파일은 스키마 검증을 통과하고도 여전히 유효하지 않은 인보이스일 수 있으며, 업무 규칙 계층이 존재하는 이유가 그것입니다

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:Version에서든 factur-x.xml의 BT-24에서든 1.0으로 유지된다고 밝히고 있습니다. 공식 ZUGFeRD 2.5 예제 모음이 이를 확인해 줍니다. 모든 예제가 fx:Version이 1.0인 urn:factur-x.eu:1p0 가이드라인 URN 아래에 factur-x.xml을 임베드합니다. 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 바이트를 연관 파일로 임베드하고, factur-x.xml을 카탈로그 /AF 배열과 /Names /EmbeddedFiles 트리에 등록하며, 그에 맞는 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 컨테이너
    Pdf.BeginDoc;
    // EN 16931 CII 인보이스 XML을 factur-x.xml로 임베드합니다
    InvoiceXML := BuildInvoiceXML;         // 여러분의 업무 계층이 만든 값
    Pdf.AddFacturXAssociatedFile(InvoiceXML, 'EN 16931');
    // 선택 사항: 보조 표현으로 UBL 뷰를 첨부합니다
    UBLXML := BuildUBLXML;
    Pdf.AddUBLSupplementaryFile(UBLXML);   // factur-xubl.xml, Alternative
    // ... 눈에 보이는 인보이스 페이지를 여기서 그립니다 ...
    Pdf.EndDoc;
  finally
    Pdf.Free;
  end;
end;

기본값은 명세를 따릅니다. 파일 이름은 factur-x.xml, 관계는 Alternative, 버전은 1.0입니다. AddUBLSupplementaryFile은 Factur-X 1.09 명세 6.4절을 구현한 것으로, 같은 인보이스의 UBL 표현이 MIME 타입 application/xml과 Alternative 관계를 지닌 factur-xubl.xml이라는 두 번째 첨부로 함께 실리도록 허용합니다. 여기서는 순서가 중요하며, HotPDF는 명세가 함의하는 제약을 강제합니다. UBL 파일은 보조 뷰이지 결코 주 인보이스가 아니므로, AddFacturXAssociatedFile이 CII 원본을 등록한 뒤에 추가되어야 합니다. 그려진 페이지와 임베드된 XML은 같은 인보이스를 말해야 한다는 점도 유념하십시오 — 한 가지 총액을 표시하면서 다른 총액을 인코딩하는 PDF야말로 하이브리드 인보이스가 막으려고 설계된 바로 그 실패 양상입니다. OutputIntent와 폰트 임베딩을 포함한 이 워크플로의 PDF/A-3 측면은 Delphi의 PDF/A, PDF/X, PDF/UA 검증을 다룬 자매 글에서 더 깊이 다룹니다

들어온 PDF에서 Factur-X 인보이스를 어떻게 탐지할까요?

THotPDF.DetectFacturXInvoice는 제도의 수신 측 질문에 답합니다. 아무 PDF나 로드해서 그것이 하이브리드 인보이스인지, 어떤 프로파일을 선언하는지, 어떤 첨부를 지니고 있는지를 한 번의 호출로 알아냅니다. 이 함수는 임베드된 XML 파일 이름, XMP가 선언한 문서 파일 이름과 유형, 버전 토큰, 준수 수준, 가이드라인 URN, AFRelationship 값을 담은 THPDFFacturXInvoiceInfo 레코드를 반환합니다. 두 개의 추가 필드가 보조 뷰를 알려 줍니다. UBLFileName은 factur-xubl.xml 첨부가 있을 때 채워지고, EDIFACTFileName은 EDIFACT 뷰가 함께 실렸을 때 채워집니다 — 둘 다 탐지 판정에는 영향을 주지 않습니다. 보조 표현은 주 CII 인보이스 없이는 존재할 수 없기 때문입니다

DetectFacturXInvoice가 THPDFFacturXInvoiceInfo 레코드를 채우고 판정을 반환하는 Delphi의 Factur-X 인보이스 탐지 다이어그램
탐지는 준수 검증기가 읽는 것과 같은 신호를 읽은 다음 프로파일과 선언된 모든 첨부를 보고합니다
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 확장 스키마와 연관 파일 등록부입니다. 같은 로드 문서 객체는 검사나 수정을 위해 XMP와 Info 딕셔너리 메타데이터도 노출하며, 그 워크플로는 로드된 PDF 문서의 메타데이터 편집을 다룬 글에서 설명합니다

업무 규칙인가 스키마인가: EN 16931 검증은 무슨 뜻인가?

HPDFEInvoiceValidator 유닛의 독립 함수인 HPDFValidateEInvoice는 두 검증 계층을 차례로 돌립니다. 먼저 ValidateFacturXInvoice로 컨테이너 구조를 검사하고, 그다음 추출된 인보이스 XML에 EN 16931 Schematron 업무 규칙을 돌립니다. HotPDF는 공식 Factur-X 1.09 규칙 파일 다섯 개, 즉 MINIMUM, BASIC WL, BASIC, EN 16931, EXTENDED를 Lib/resources/Schematron/ 아래에 함께 제공하며, HPDFSchematronFileForProfile이 탐지된 준수 수준에 맞는 파일을 고르므로 EN 16931 인보이스는 자동으로 Factur-X_1.09_EN16931.sch로 검사됩니다. 이 함수는 컨테이너가 유효하고 발동한 업무 규칙이 하나도 없을 때에만 True를 반환합니다

컨테이너 구조 검사를 먼저 돌리고 이어서 Schematron 업무 규칙을 돌리며 건너뛴 XPath 2.0 규칙을 보고하는 EN 16931 전자 인보이스 검증의 HotPDF 다이어그램
컨테이너 검사가 먼저, Schematron 규칙이 그다음이며, 보고서는 규칙 집합이 얼마나 실제로 실행되었는지 정확히 밝힙니다
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-45, BR-CO-17을 비롯한 EN 16931 명명 체계를 — 규칙 컨텍스트 XPath, 테스트 표현식, 그리고 Schematron flag 속성을 반영한 심각도와 함께 담고 있습니다. 덕분에 보고서로 바로 조치할 수 있습니다. 맨 통과/실패 대신 어떤 업무 용어가 앞뒤가 맞지 않는지 정확히 알게 되며, 고객이 인보이스가 왜 반송되었는지 물을 때 지원팀에 필요한 것이 바로 그것입니다. 보존용 PDF에 대해 이미 구조화된 준수 보고서를 만들고 있는 팀이라면 이 결과를 PDF 프리플라이트 보고서 자동화를 다룬 글에서 설명한 같은 파이프라인에 접어 넣을 수 있습니다

검증기가 멈추는 지점, 솔직하게

Schematron 엔진은 XPath 1.0을 평가하는 MSXML 위에 지어졌습니다. EN 16931 규칙 파일은 queryBinding="xslt2"를 선언하고 424개의 단언을 담고 있는데, 그 대부분은 string-length나 substring-after 같은 XPath 1.0 함수만 씁니다. 그런 규칙은 그대로 실행됩니다. 소수는 xs:decimal 형 변환이나 exists() 같은 XPath 2.0 구문에 기댑니다. HotPDF는 각 테스트 표현식을 미리 훑어 알려진 XPath 2.0 토큰을 찾아내고, 그런 규칙은 조용히 어긋날 평가를 시도하는 대신 심각도 정보와 함께 Skipped로 표시합니다. 요약 레코드가 평가된 수와 건너뛴 수를 따로 보고하므로, 여러분의 로그는 규칙 집합이 얼마나 실행되었는지 언제나 정확히 밝힙니다. MSXML 바인딩을 쓸 수 없는 Delphi 7에서는 이 유닛이 컨테이너 판정을 흐트러뜨리지 않으면서 엔진을 사용할 수 없다고 보고하는 스텁으로 컴파일됩니다. 완전한 엔진은 Delphi XE2 이상이 필요합니다

두 번째 경계는 그보다 더 중요합니다. HPDFValidateEInvoice의 초록불은 컨테이너 구조가 준수하고 평가된 업무 규칙 중 발동한 것이 없다는 뜻입니다 — 세무 준수를 보증하는 것이 아닙니다. 국가별 확장 규칙, 수신자별 요구 사항, 인보이스 내용을 둘러싼 법적 의무는 EN 16931 코어 위에, 그리고 어떤 라이브러리의 바깥에 있습니다. 이 검증기는 구조가 망가진 인보이스를 보낼 편지함에서 걸러 내고 앞뒤가 맞지 않는 인보이스를 받은 편지함에서 표시해 주는 관문으로 여기고, 인증이 요구될 때는 릴리스 파이프라인에서 완전한 XPath 2.0 검증기를 돌리며, 준수 문제는 세무 자문가에게 맡기십시오. 여기서 보인 임베딩, 탐지, 검증 API는 Delphi 및 C++Builder용 표준 HotPDF Delphi Component의 일부이며, ZUGFeRD와 Factur-X의 일곱 가지 프로파일을 모두 다루는 완전한 E-Invoice 예제도 함께 제공됩니다