Технічна стаття

Електронні інвойси ZUGFeRD та Factur-X у Delphi за допомогою HotPDF

HotPDF генерує, виявляє та валідує електронні інвойси ZUGFeRD та Factur-X у Delphi та C++Builder. Метод AddFacturXAssociatedFile вбудовує XML-файл інвойсу стандарту EN 16931 у контейнер PDF/A-3, DetectFacturXInvoice розпізнає вхідний гібридний інвойс, а HPDFValidateEInvoice запускає офіційні бізнес-правила Schematron EN 16931 для вбудованого XML-коду. У цій статті розглядаються всі три кроки, а також чесно описується, де зупиняються можливості движка валідації

Причина впровадження цих функцій є законодавчою, а не технічною. Німеччина та Франція поступово запроваджують обов'язкові структуровані електронні інвойси для внутрішніх операцій B2B, і обидва законодавчі акти сходяться на одній гібридній моделі: інвойс PDF, який уже створює ваша ERP-система, повинен містити всередині машинозчитувану копію у форматі XML. Бухгалтерські програми або ERP-додатки на Delphi, які сьогодні генерують звичайні PDF-інвойси, мають жорсткий термін для переходу на 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.xml для німецького варіанта XRechnung має бути зареєстрований у масиві каталогу /AF та дереві /Names /EmbeddedFiles із записом AFRelationship, що вказує тип зв'язку: Alternative означає, що XML є еквівалентним представленням візуального інвойсу, що є вимогою для більшості профілів. По-третє, схема розширення Factur-X XMP повинна містити назву вбудованого файлу, його рівень відповідності та тип документа, щоб система прийому могла класифікувати інвойс взагалі без аналізу самого XML-коду

Самі дані 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 стверджує, що номер версії в URI схеми розширення PDF/A не пов'язаний із номером версії специфікації даних XML, і що версії екземплярів інвойсів Factur-X залишаються 1.0 — як в XMP fx:Version, так і в полі BT-24 файлу factur-x.xml. Офіційний набір прикладів ZUGFeRD 2.5 підтверджує це: кожен зразок вбудовує factur-x.xml під URN urn:factur-x.eu:1p0 з версією fx:Version, рівною 1.0. Те, що насправді додає версія 2.5, знаходиться всередині моделі даних XML — нові елементи, такі як піделементи рядків інвойсу та ідентифікатори партій, що є бізнес-рівнем, який генерує ваш додаток, а не рівнем контейнера, яким керує бібліотека PDF

HotPDF враховує цей факт напряму. Метод AddFacturXAssociatedFile приймає аргумент Version та нормалізує його через HPDFFacturXNormalizeVersion перед записом XMP, тож викликач, передаючи аліас 2p5, все одно створює токен відповідно до специфікації замість вигадування версії XMP 2p5, яку не прийме жоден валідатор. Помічники виявлення так само сприймають 2p5 як еквівалент при зчитуванні, тому процеси залишаються симетричними. Якщо якась бібліотека чи інструмент стверджує, що записує спеціальний контейнер ZUGFeRD 2.5, сама специфікація каже, що такого поняття не існує

Вбудовування XML інвойсу з Delphi

Метод THotPDF.AddFacturXAssociatedFile перетворює звичайний процес генерації PDF/A-3 на створення інвойсу Factur-X за один виклик: він вбудовує байти XML як асоційований файл, реєструє factur-x.xml у масиві каталогу /AF та дереві /Names /EmbeddedFiles, а також записує відповідні метадані розширення Factur-X XMP. Ви малюєте людиночитану сторінку інвойсу за допомогою звичайного API полотна HotPDF, а робота з контейнером зводиться до одного рядка між 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 реалізує розділ 6.4 специфікації Factur-X 1.09, який дозволяє додавати представлення цього ж інвойсу у форматі UBL як другий вкладений файл з іменем factur-xubl.xml, MIME-типом application/xml та зв'язком Alternative. Порядок додавання має значення, і HotPDF забезпечує виконання обмежень специфікації: файл UBL є допоміжним представленням, а не основним інвойсом, тому його слід додавати після того, як AddFacturXAssociatedFile зареєструє CII-майстер. Зверніть увагу, що намальована сторінка та вбудований XML повинні містити однакові дані — PDF, який відображає одну суму, а в коді записує іншу, є саме тією помилкою, для запобігання якій і було створено гібридні інвойси. Тема роботи з PDF/A-3, включаючи OutputIntent та вбудовування шрифтів, детально описана у статті про валідацію PDF/A, PDF/X та PDF/UA в Delphi

Як виявити інвойс Factur-X у вхідному файлі PDF?

Метод THotPDF.DetectFacturXInvoice вирішує завдання на приймальній стороні: завантажте будь-який PDF та дізнайтеся за один виклик, чи є він гібридним інвойсом, який профіль він декларує та які вкладення містить. Функція повертає запис THPDFFacturXInvoiceInfo з іменем вбудованого XML-файлу, назвою та типом документа в XMP, токеном версії, рівнем відповідності, URN та значенням AFRelationship. Два додаткових поля повідомляють про допоміжні представлення: UBLFileName заповнюється, коли присутнє вкладення factur-xubl.xml, а EDIFACTFileName — при наявності представлення EDIFACT; жодне з них не впливає на загальний вердикт виявлення інвойсу, оскільки допоміжні представлення не можуть існувати без основного інвойсу 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;

Бізнес-правила чи схема: що означає валідація EN 16931?

Функція HPDFValidateEInvoice у модулі HPDFEInvoiceValidator запускає обидва рівні валідації по черзі: спочатку перевірку структури контейнера через ValidateFacturXInvoice, а потім перевірку бізнес-правил Schematron EN 16931 для вилученого XML інвойсу. 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 тільки тоді, коли контейнер є валідним і жодне бізнес-правило не було порушено

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, вираз перевірки та ступінь важливості (severity), що відповідає атрибуту flag у Schematron. Це робить звіт корисним на практиці: замість простого результату «так/ні» ви бачите конкретний неузгоджений бізнес-термін, що спрощує роботу служби підтримки при аналізі відхилених інвойсів. Команди, які вже генерують звіти про відповідність для архівних PDF, можуть інтегрувати ці результати в той самий робочий процес, що змальовано у статті про автоматизацію звітів перевірки PDF

Де зупиняються можливості валідатора, чесно

Движок Schematron побудовано на базі 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 і позначає ці правила як Skipped із рівнем важливості info замість спроби виконання, яка б завершилася помилкою. Підсумковий запис повідомляє про кількість виконаних та пропущених правил окремо, тому ваш лог завжди точно показує обсяг виконаної перевірки. У Delphi 7, де прив'язки MSXML недоступні, модуль компілює заглушку, яка повідомляє про недоступність движка без зміни вердикту щодо контейнера; для повноцінної роботи потрібна версія Delphi XE2 або новіша

Друге обмеження є ще важливішим. Успішний результат перевірки від HPDFValidateEInvoice означає, що структура контейнера є правильною і жодне з перевірених бізнес-правил не було порушено — це не є юридичним підтвердженням податкової відповідності. Національні правила розширення, вимоги конкретних одержувачів та юридичні зобов'язання щодо вмісту інвойсу стоять вище за ядро стандарту EN 16931 та знаходяться поза межами будь-якої бібліотеки. Розглядайте валідатор як фільтр, який запобігає надсиланню структурно пошкоджених документів та виявляє некоректні документи у вхідній пошті, використовуйте повний валідатор XPath 2.0 у процесі випуску, якщо цього вимагає сертифікація, та залиште питання юридичної відповідності вашим податковим консультантам. Описані тут методи API для вбудовування, виявлення та валідації постачаються у складі HotPDF Component для Delphi та C++Builder разом із повним прикладом роботи з електронними інвойсами для всіх семи профілів ZUGFeRD та Factur-X