Счёт в формате 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. Каждый из них перехватывает именно тот класс дефектов, который невидим для другого, что является основной причиной, по которой серьёзный стенд валидации запускает оба инструмента и считает файл пригодным к отправке только тогда, когда оба согласны
Матрица валидации
Чтобы доказать, что библиотека создаёт файлы, проходящие через оба шлюза, стенд строит матрицу. Шесть профилей счетов охватывают диапазон, с которым европейский конвейер встречается на практике: 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;
// ... 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 валидируется по 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 - нет. Опустите любой из них, и счёт будет хорошо сформированным, соответствующим схеме и отклонённым
Третьим является правило 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); // 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, управляет обоими валидаторами в выходном каталоге и выводит таблицу успехов и отказов. Та же двухэтапная схема - генерация с помощью библиотеки и проверка внешними валидаторами - это то, что задание непрерывной интеграции должно выполнять при каждом изменении генерации счетов, поскольку единственный способ узнать, что файл удовлетворяет обоим слоям, - это спросить оба инструмента
Если вашему конвейеру также необходимо сертифицировать контейнер перед подписанием, предпечатная сторона этой работы описана в нашем руководстве по предпечатной проверке PDF/A и PDF/UA в Delphi, а более широкий процесс сертификации с последующей подписью описан в стенде соответствия и подписи. Оба основаны на том же пути генерации, который поставляется как часть Delphi PDF Library для Delphi и C++Builder вместе с API PDF/A, связанных файлов и метаданных, использованными здесь