Техническая статья

Валидация электронных счетов Delphi с помощью veraPDF и Mustang

Счёт в формате Factur-X или ZUGFeRD - это два документа под одним именем файла. Внешний документ является контейнером PDF/A-3, который архивная программа должна принимать в течение следующих десяти лет. Внутренний документ - это XML-счёт, который система бухгалтерского учёта покупателя должна разбирать в соответствии с EN 16931. Ошибка, приводящая к попаданию неверных счетов в производство, заключается в убеждённости, что правильно созданный первый автоматически обеспечивает правильность второго. Это не так. Файл может быть безупречным PDF/A-3 и при этом содержать XML, который не примет ни одна налоговая служба, а также может содержать образцовый XML EN 16931 внутри контейнера, не прошедшего архивную валидацию. Два слоя проверяются двумя разными инструментами, которые ничего не знают друг о друге, и реальный конвейер должен удовлетворять обоим

Два валидатора, два разных вопроса

veraPDF является эталонной реализацией для PDF/A. Направьте его на счёт и он ответит на один вопрос: является ли это соответствующим файлом PDF/A-3. Он проверяет то, что важно для ISO 19005-3. Встроены ли все шрифты. Есть ли OutputIntent. Объявляют ли метаданные XMP правильный раздел и уровень соответствия. Для электронного счёта он также проверяет сантехнику связанных файлов, которую требует PDF/A-3, поскольку XML передаётся как встроенный файл с /AFRelationship и записью в массиве /AF каталога документов. veraPDF не говорит ничего о том, сходится ли итог счёта, поскольку это не его компетенция

Mustang - это валидатор с открытым исходным кодом от проекта Mustangproject. Он задаёт ортогональный вопрос: является ли встроенный XML допустимым счётом. Он проверяет XML на соответствие схеме для объявленного профиля, а затем применяет бизнес-правила EN 16931 и наслоённые поверх них страновые наборы правил, в том числе CIUS XRechnung. Он проверяет, присутствует ли идентификатор НДС продавца, когда этого требуют итоги, что суммы скидок и надбавок согласуются с итогом документа, что URI профиля в XML соответствует заявленному типу файла. Mustang не заботится о том, встраивает ли окружающий PDF свои шрифты, поскольку это работа veraPDF

Ни один инструмент не является надмножеством другого. veraPDF пропускает структурно совершенный контейнер вокруг бессмысленного XML. Mustang пропускает совершенный XML, завёрнутый в контейнер с отсутствующим OutputIntent. Каждый из них перехватывает именно тот класс дефектов, который невидим для другого, что является основной причиной, по которой серьёзный стенд валидации запускает оба инструмента и считает файл пригодным к отправке только тогда, когда оба согласны

PDF Library for Delphi: многослойная диаграмма файла электронного счёта Factur-X: внешний архивный контейнер PDF/A-3 с обвязкой /AFRelationship и /AF, проверяемый veraPDF, и внутренний XML счёта factur-x.xml по EN 16931, проверяемый Mustang, причём ни один инструмент не покрывает другой слой
Внешний слой PDF/A-3 отвечает перед veraPDF, а Mustang судит встроенный XML EN 16931, причём ни одна из проверок не подразумевает другую

Матрица валидации

Чтобы доказать, что библиотека создаёт файлы, проходящие через оба шлюза, стенд строит матрицу. Шесть профилей счетов охватывают диапазон, с которым европейский конвейер встречается на практике: Factur-X EN 16931, Factur-X BASIC, французский вариант Factur-X EXTENDED B2B, XRechnung 3.0, ZUGFeRD 1.0 COMFORT и ZUGFeRD 2.0 BASIC. Каждый профиль генерируется против двух уровней соответствия PDF/A - 3b и 3u, поскольку требования уровней B и U расходятся в части отображения Unicode, и файл, прошедший один из них, может провалить другой. Шесть профилей умножить на два уровня - двенадцать файлов, каждый из которых создаётся в безголовом режиме по тому же кодовому пути, что и образец с GUI, так что артефакты, проходящие тестирование, не подготовлены вручную для теста

Генератор записывает все двенадцать файлов, и скрипт подаёт каждый из них в оба валидатора. При первом полном прогоне veraPDF прошёл все двенадцать. Сантехника контейнера была верной во всех случаях: связанные файлы зарегистрированы, соответствие XMP объявлено, намерения вывода на месте. Mustang прошёл восемь. Четыре счёта были структурно допустимыми файлами PDF/A-3, содержащими XML, который отклонил валидатор бизнес-правил, - именно такое расхождение и должен выявить двухинструментальный подход. Если бы стенд доверился только veraPDF, эти четыре выглядели бы завершёнными

Два исправления, закрывшие пробел

Четыре сбоя Mustang имели две различные причины, и исправление каждой из них - деталь, которую стоит знать, прежде чем самостоятельно генерировать эти профили

Первой была французская версия профиля Factur-X EXTENDED B2B. Исходный генератор передавал внутреннюю метку в качестве уровня соответствия и внутренний URI в качестве руководящего принципа, и Mustang отклонил файл с ошибкой недопустимого значения соответствия, за которой последовала ошибка неподдерживаемого типа профиля. Причина в том, что поле XMP fx:ConformanceLevel - это не свободный текстовый слот для вашего собственного наименования профиля. Factur-X определяет ровно пять стандартных значений для него: MINIMUM, BASIC WL, BASIC, EN 16931 и EXTENDED. Французский бизнес-счёт B2B с точки зрения метаданных XMP всё равно является документом профиля EXTENDED. Французский характер счёта выражается не изобретением шестого значения соответствия. Он выражается кодом страны, FR, и идентификатором руководящего принципа внутри XML, который должен нести префикс urn:cen.eu:en16931:2017#conformant#, обозначающий CIUS, соответствующий EN 16931. Передача стандартного значения EXTENDED с FR в качестве кода страны и правильного URI руководящего принципа сделала файл соответствующим

В API библиотеки это вызов AddFacturXAssociatedFileFromString с выровненными уровнем соответствия, страной и руководящим принципом. Аргумент уровня соответствия несёт стандартный токен, аргумент кода страны несёт FR, а URI руководящего принципа находится в передаваемых байтах XML

var
  FileID: Integer;
begin
  PDF.SetPDFAMode(5);            // PDF/A-3b
  PDF.NewDocument;
  // ... нарисовать страницу счёта для чтения человеком ...
  // ExtendedXML содержит URN рекомендации EN 16931 вида
  //   urn:cen.eu:en16931:2017#conformant#urn:factur-x.eu:1p0:extended
  FileID := PDF.AddFacturXAssociatedFileFromString(
    ExtendedXML,
    'EXTENDED',          // стандартный fx:ConformanceLevel, а не внутренний ярлык
    'factur-x.xml',
    'Factur-X EXTENDED invoice',
    'Alternative',       // /AFRelationship
    '1.0',
    'FR');               // France B2B помечается кодом страны, а не уровнем соответствия
  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 валидируется по XSD :1p0, который строже относится к кардинальности, чем следует из прозаических резюме. XSD требует, чтобы сводка расчётов заголовка, ram:SpecifiedTradeSettlementMonetarySummation, содержала ровно по одному экземпляру ram:ChargeTotalAmount и ram:AllowanceTotalAmount. Сгенерированный XML опустил оба, поэтому Mustang сообщил, что элементы должны встречаться ровно один раз. Они не являются необязательными, если схема говорит, что minOccurs равен единице. Добавление обоих в порядке последовательности XSD, сразу после ram:LineTotalAmount, со значением 0.00 при отсутствии надбавок или скидок удовлетворило схеме. Ноль - это присутствующий элемент; отсутствующий элемент - это нарушение схемы. Применив эти два исправления, матрица перешла к двенадцати из двенадцати по Mustang, сохранив двенадцать из двенадцати по veraPDF

Поля XRechnung, превращающие недействительный счёт в действительный

XRechnung заслуживает отдельного упоминания, поскольку его немецкий CIUS добавляет бизнес-правила, отсутствующие в базовом наборе EN 16931, и они дают сбои способами, при которых документ на первый взгляд выглядит нормально. Два из них касаются электронных адресов. BT-34 - это электронный адрес продавца, а BT-49 - электронный адрес покупателя, конечные точки маршрутизации, которые немецкий портал государственного сектора использует для доставки и подтверждения получения счёта. Базовая модель EN 16931 считает их необязательными. XRechnung - нет. Опустите любой из них, и счёт будет хорошо сформированным, соответствующим схеме и отклонённым

PDF Library for Delphi: итог проверочной матрицы: шесть профилей счетов от Factur-X EN 16931 до ZUGFeRD 2.0 BASIC, созданных на уровнях 3b и 3u, дают двенадцать файлов; veraPDF проходит двенадцать из двенадцати, Mustang — только восемь, а два исправления — токен соответствия и обязательные суммирующие элементы — устраняют разрыв
Шесть профилей на двух уровнях соответствия дают двенадцать файлов: все двенадцать зелёные у veraPDF и лишь восемь у Mustang

Третьим является правило BR-DE-6, которое требует наличия телефонного номера контакта продавца. Это то поле, которое разработчик опускает, потому что оно кажется презентационным, а не данными, и его отсутствие приводит к ошибке валидации, указывающей на группу контактов продавца, а не на что-то очевидно отсутствующее. Предоставление BT-34, BT-49 и телефонного номера продавца - это то, что переводит файл XRechnung из недействительного в действительный по Mustang, и ни одно из этих изменений не меняет того, что видит veraPDF, поскольку все три находятся в XML

Подключение вывода библиотеки к валидатору

Архитектурный смысл стенда обобщается на любую бизнес-систему. Библиотека PDF записывает соответствующий контейнер и встраивает XML. Она не должна и не пытается быть органом власти по бизнес-правилам EN 16931. ValidateFacturXInvoice в библиотеке проверяет согласованность контейнера - что массив /AF каталога, дерево имён встроенных файлов, XMP DocumentFileName, профиль, руководящий принцип и /AFRelationship все согласуются, - но не валидирует налоговые коды или сверяет суммы. Правильное разделение труда состоит в том, чтобы бизнес-система извлекала XML и передавала его специализированному валидатору счетов, точно так же, как стенд передаёт его Mustang

Обратное чтение файла показывает, что было в него записано. DetectFacturXInvoice сообщает, был ли распознан счёт, а GetFacturXInvoiceInfo считывает поля метаданных по тегу: тег 1 - имя встроенного файла, тег 2 - XMP DocumentFileName, тег 5 - уровень соответствия, тег 6 - идентификатор руководящего принципа, тег 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);  // ID рекомендации XML
    Writeln('Profile:   ', Profile);
    Writeln('Guideline: ', Guideline);
    // Передать исходный XML специализированному валидатору EN 16931 / Mustang
    Result := PDF.ExtractFacturXXMLToString;
  end;
end;

ExtractFacturXXMLToString возвращает необработанные байты XML в виде AnsiString, готового для записи в файл или потоковой передачи в процесс валидатора. В испытательном стенде целью является Mustang, вызываемый через его командную строку jar-файла, с veraPDF, запущенным за тот же проход по тому же файлу. Монтаж невелик: консольный генератор, EInvoiceValidation.dpr, записывает двенадцать файлов с использованием общей модели счёта из образца, а скрипт, run-validation.ps1, управляет обоими валидаторами в выходном каталоге и выводит таблицу успехов и отказов. Та же двухэтапная схема - генерация с помощью библиотеки и проверка внешними валидаторами - это то, что задание непрерывной интеграции должно выполнять при каждом изменении генерации счетов, поскольку единственный способ узнать, что файл удовлетворяет обоим слоям, - это спросить оба инструмента

Если вашему конвейеру также необходимо сертифицировать контейнер перед подписанием, предпечатная сторона этой работы описана в нашем руководстве по предпечатной проверке PDF/A и PDF/UA в Delphi, а более широкий процесс сертификации с последующей подписью описан в стенде соответствия и подписи. Оба основаны на том же пути генерации, который поставляется как часть Delphi PDF Library для Delphi и C++Builder вместе с API PDF/A, связанных файлов и метаданных, использованными здесь

Диаграмма конвейера PDF Library for Delphi, связывающая вывод библиотеки с внешними валидаторами: генератор headless-режимом пишет invoice.pdf, veraPDF проверяет контейнер, ExtractFacturXXMLToString подаёт в Mustang бизнес-правила EN 16931, а шлюз выпуска возвращает ноль только когда оба валидатора проходят
Конвейер генерирует в headless-режиме, передаёт один и тот же артефакт обоим валидаторам и отправляет файл только при общем зелёном вердикте