PDFlibPas, Delphi PDF Library от losLab, проверяет бизнес-правила EN 16931, XRechnung 3.0.2 и Peppol BIS Billing 3.0 нативно, на Pascal. TPDFlib.ValidateInvoiceBusinessRulesBuiltIn читает XML, встроенный в PDF со счётом Factur-X или ZUGFeRD, и возвращает JSON-отчёт с официальными идентификаторами правил — без Java-рантайма, Saxon или Schematron-инструментария. Отчёт сам называет себя подмножеством, и эта пометка — важнейшее поле во всём отчёте
Давление за этим в основном немецкое. С января 2025 каждая компания в Германии обязана уметь принимать структурированные B2B электронные счета, а обязанность отправлять подступает поэтапно. Принимающая сторона Delphi-приложения для бухгалтерии или ERP внезапно должна ответить на вопрос, которого раньше не задавала: действительно ли XML внутри этого PDF — валидный счёт EN 16931? Официальный ответ дают Schematron-артефакты, скомпилированные в XSLT 2.0, или KoSIT-валидатор для XRechnung, и оба живут на JVM. Таскать и патчить JRE рядом с каждой десктопной установкой только чтобы отвергнуть плохой счёт — расход, который многие команды предпочли бы не нести
Что именно проверяет встроенный валидатор?
Нативный валидатор отображает XML счёта на бизнес-термины EN 16931 и прогоняет документированное подмножество правил из артефактов валидации EN 16931 1.3.16, сообщая каждую находку под её официальным идентификатором. Поддерживаются оба синтаксиса: UN/CEFACT CII D16B, который встраивают Factur-X и ZUGFeRD, и OASIS UBL 2.1 Invoice и CreditNote. Покрываемые области:
- Обязательные термины: идентификатор спецификации, номер счёта, дата выставления, код типа, валюта, имена продавца и покупателя, адреса, итоги документа, разбивка НДС и строки (BR-01 to BR-16, BR-21 to BR-28, BR-CO-04, BR-CO-18, BR-CO-26, BR-45 to BR-48)
- Вычисления: итоги строки, скидки, надбавки, налога и к оплате, плюс налоговая база и сумма налога каждой категории НДС (BR-CO-10 to BR-CO-17, BR-xx-08, BR-xx-09)
- Правила категорий НДС для S, Z, E, AE, K, G, O, L и M: ставки по строкам, причины освобождения и идентификаторы НДС, которых требует каждая категория
- Все справочники кодов BR-CL-01 to BR-CL-26, включая 2100 с лишним кодов единиц UN/ECE Rec 20/21 за BR-CL-23, и правила двух десятичных знаков BR-DEC-01, -05, -09 to -20 и -23
- Правила профилей: XRechnung 3.0.2 (BR-DE-1 to BR-DE-31, BR-DEX-01 to BR-DEX-14) на CII и UBL, а также ядро, общие, справочниковые и национальные правила Peppol BIS Billing 3.0.20 на UBL
Охват сообщается, а не подразумевается. Без файла схемы стадия схемы подтверждает лишь корректность XML и корневой элемент. Каждая стадия перечисляет правила вне подмножества в поле notCovered, а GetBuiltInInvoiceRuleCatalog возвращает всю карту правил как JSON, чтобы ваш UI мог показать пользователю, что именно покрывает зелёный результат
Ещё одну границу легко пропустить. Документы Factur-X MINIMUM и BASIC WL — не счета EN 16931, поэтому валидатор не применяет к ним правила EN 16931. Вместо этого он добавляет информационную находку BuiltIn.Coverage.Profile, которая так и говорит, — честный ответ и заодно подсказка: по немецким правилам эти два профиля электронными счетами и не считаются
Валидация встроенного XML гибридного PDF
ValidateInvoiceBusinessRulesBuiltIn принимает один аргумент — имя встроенного файла, и пустая строка — нормальный выбор. Тогда PDFlibPas берёт вложение, которое определило обнаружение Factur-X; для PDF без метаданных FacturX он откатывается к единственному вложению .xml. Несколько кандидатов .xml или два вложения с запрошенным именем дают ошибку Invoice.AttachmentAmbiguous вместо угадывания, декодирование ограничено 64 МБ, а отсутствующий счёт ставит LastErrorCode в 401
uses
System.SysUtils, System.JSON, PDFlibrary;
procedure CheckInboundInvoice(const FileName: string);
var
PDF: TPDFlib;
Report: TJSONObject;
begin
PDF := TPDFlib.Create;
try
if PDF.LoadFromFile(FileName, '') <> 1 then
raise Exception.CreateFmt('Cannot open %s (error %d)',
[FileName, PDF.LastErrorCode]);
// Пустое имя: проверяем обнаруженное вложение Factur-X / ZUGFeRD
Report := TJSONObject.ParseJSONValue(
PDF.ValidateInvoiceBusinessRulesBuiltIn('')) as TJSONObject;
try
Writeln('profile: ', Report.GetValue<string>('profile'));
Writeln('valid: ', Report.GetValue<Boolean>('valid'));
Writeln('coverage: ', Report.GetValue<string>('coverage')); // "subset"
Writeln('full: ', Report.GetValue<Boolean>('fullConformance')); // здесь никогда не True
finally
Report.Free;
end;
finally
PDF.Free;
end;
end;
Верхний уровень отчёта несёт standard, version, profile, complete, valid, fullConformance, coverage, стадии schema и schematron и объект package, который во встроенном режиме равен null. Каждая стадия сообщает статус, движок, число выполненных правил, свой охват и массив issues. У каждой находки есть ruleID, fieldPath, severity, category, line и message
Читать находки по категории, а не по тексту
Поле category — то, что делает отчёт пригодным для реальной почты входящих, потому что оно подсказывает, кто должен чинить проблему. Значения: syntax, schema, cardinality, calculation, vat, codelist, decimal, profile, coverage, processing и integrity. Находка calculation обычно указывает на округление у отправителя, codelist — на мастер-данные, а profile — на поле XRechnung вроде buyer reference
procedure ListIssues(Report: TJSONObject);
var
Item: TJSONValue;
Issue: TJSONObject;
begin
for Item in Report.GetValue<TJSONArray>('schematron.issues') do
begin
Issue := Item as TJSONObject;
if Issue.GetValue<string>('severity') = 'info' then
Continue; // пометки покрытия вроде BuiltIn.Coverage.Profile
Writeln(Format('[%s] %s at %s: %s', [
Issue.GetValue<string>('category'),
Issue.GetValue<string>('ruleID'),
Issue.GetValue<string>('fieldPath'),
Issue.GetValue<string>('message')]));
end;
end;
Тот же цикл стоит прогнать и по schema.issues: ошибки корректности XML и корневого элемента попадают туда, и стадия бизнес-правил никогда не видит документ, который не смогла разобрать
Почему один и тот же счёт проходит в CII и падает в UBL?
Официальные артефакты не применяют одинаковый числовой допуск в обоих синтаксисах, и PDFlibPas копирует каждое правило из XSLT своего синтаксиса, а не гармонизирует их. BR-CO-17 — самый наглядный случай. Он проверяет, что сумма налога категории НДС равна налоговой базе, умноженной на ставку, с округлением до двух знаков, и оба синтаксиса допускают расхождение в одну денежную единицу. Артефакт CII записывает это как <= и >=; артефакт UBL использует abs со строгим <
Возьмите налоговую базу 100.00 при 19 процентах, то есть ожидаемый налог 19.00, и счёт, который декларирует 20.00. В CII 20.00 минус 1 равно 19.00, что удовлетворяет <=, и BR-CO-17 проходит. В UBL то же сравнение строгое: 19.00 не меньше 19.00, и BR-CO-17 падает. Такой счёт никто не отправляет нарочно, но генераторы с багом округления сидят ровно на таких границах, а тестовый набор, покрывающий только один синтаксис, этого не заметит
Различия идут дальше BR-CO-17. BR-xx-08 в CII требует точного равенства после округления до двух знаков, тогда как UBL допускает ±1 и срабатывает, только когда у категории реально есть строки, скидки или надбавки. BR-O-08 точен в обоих. Артефакт CII определяет BR-AF-09 и BR-AG-09 как true(), так что там они не падают никогда, а UBL проверяет их с ±1. И когда в разбивке НДС CII вообще нет ставки, правила не-O категорий проходят формально, потому что XPath every $rate по пустой последовательности истинен. Нативному коду приходится пропускать их так же, иначе он сыплет ошибками на официальных примерах
Вторая ловушка касается идентификаторов правил. Префикс категории правила — не код категории НДС: K отображается в BR-IC, L в BR-AF, M в BR-AG. Выводить идентификатор из кода категории — получать BR-K-09 и BR-K-10, идентификаторы, которых в EN 16931 нет. Если ваше приложение сопоставляет идентификаторы правил с текстами подсказок, берите идентификатор из отчёта и никогда не собирайте его из кода налоговой категории
Как включаются правила XRechnung и Peppol?
Встроенный валидатор выбирает правила профиля по идентификатору спецификации (BT-24). Идентификатор, содержащий xrechnung, включает правила CIUS XRechnung 3.0.2 на CII или UBL; идентификатор расширения urn:xeinkauf.de:kosit:extension:xrechnung_3.0 дополнительно включает правила BR-DEX. Проверки сумм строк субсчёта и НДС (BR-DEX-02, -03 и -09 to -14) выполняются только на UBL; на CII BR-DEX-15 сообщает, что строки субсчёта недоступны. Идентификатор, содержащий peppol, включает правила Peppol BIS Billing 3.0.20, которые Peppol определяет только для UBL. CII-документ, заявляющий кастомизацию Peppol, получает информационную пометку BuiltIn.Coverage.Profile о том, что эти правила не проверялись, вместо тихого прохода. Для прочих профилей CIUS используйте официальные артефакты
Одна деталь UBL стоит знания: BR-06 и BR-07 принимают в качестве имени продавца или покупателя только PartyLegalEntity/RegistrationName. Откат к PartyName/Name выглядит снисходительно, но прячет реальные провалы BR-06, поэтому проверка сделана строгой
Когда всё ещё нужен официальный Schematron?
Официальные артефакты — EN 16931 Schematron через Saxon или KoSIT-валидатор для XRechnung — нужны везде, где результат имеет юридическую или договорную силу: отказ поставщику по счёту, сертификация собственного генератора, разбор спора. Встроенный отчёт никогда не ставит fullConformance в true. Этот флаг требует верифицированного пакета правил, прохождения обеих стадий, обеих стадий, выполненных движком, исполняющим собственные правила пакета, и пустого notCovered
ValidateInvoiceBusinessRulesWithPackage выполняет тот путь. Пакет правил — каталог с манифестом rulepackage.xml, созданный CreateInvoiceRulePackage, который называет стандарт, версию, схему и Schematron-стилевой лист и записывает SHA-256 каждого файла. PDFlibPas сверяет каждый хеш при загрузке пакета и снова прямо перед тем, как движок прочитает файл, сужая окно для подмены после проверки. Пути не могут выйти за каталог пакета, а нативный XSD-загрузчик отказывает импортам, которых нет в манифесте. При настроенном xmllint импортируемые схемы читает сам xmllint, так что от подмены их защищает только проверка при загрузке
const
// SHA-256 проверенного и одобренного вами rulepackage.xml
ApprovedManifestSHA256 = '<64 hex digits from your release review>';
var
Report, Pkg: TJSONObject;
begin
// PDF: TPDFlib со счётом, уже загруженным как в первом примере
Report := TJSONObject.ParseJSONValue(
PDF.ValidateInvoiceBusinessRulesWithPackage('',
'C:\Rules\EN16931-CII-1.3.16', ApprovedManifestSHA256,
'', // пусто: нативная стадия XSD
'C:\Java\bin\java.exe', 'C:\Saxon\saxon-he.jar',
0)) as TJSONObject; // 0: 30000 ms на инструмент
try
if not (Report.FindValue('package') is TJSONObject) then
raise Exception.Create('No rule package in the report');
Pkg := Report.GetValue<TJSONObject>('package');
if not (Pkg.GetValue<Boolean>('pinned') and Pkg.GetValue<Boolean>('verified')) then
raise Exception.Create('Rule package failed its integrity check');
if Report.GetValue<Boolean>('fullConformance') then
Writeln('Conforms to the complete rule package');
finally
Report.Free;
end;
end;
Зафиксированный хеш — не необязательное украшение. Хеши файлов доказывают лишь, что файлы совпадают с манифестом, но ничто внутри пакета не доказывает подлинность самого манифеста: атакующий, способный переписать правила, перепишет и манифест вместе с ними. Зазор закрывает только хеш, зафиксированный вызывающим — в коде или в подписанной конфигурации, — а отчёт держит pinned и verified раздельно, чтобы вы видели, какая именно гарантия у вас есть
Если передать пустой JavaExecutable, стадия схемы всё равно валидирует нативно по XSD пакета, включая фасеты и identity constraints с v3.672.0, но бизнес-правила откатываются ко встроенному подмножеству, а fullConformance остаётся false. Конструкции схемы, которые нативный загрузчик не поддерживает, — скажем, substitution groups — делают загрузку неудачной с XSD.Unsupported, а не пропускаются
Куда встраиваются veraPDF и Mustang
Ни одна из этих функций не смотрит на PDF-контейнер. Соответствие PDF/A-3 по ISO 19005-3, XMP-схема расширения Factur-X и связь associated file — отдельные вопросы, и для них veraPDF и Mustang остаются референсными инструментами, как описано в статье о валидации электронных счетов с veraPDF и Mustang. Производящая сторона — от вложения XML до объявления профиля — раскрыта в статьях о сборке гибридных счетов Factur-X и ZUGFeRD на Delphi и о схемах расширения PDF/A-3 для метаданных Factur-X
Разумное разделение гоняет встроенную проверку в процессе на каждом входящем счёте, где она стоит миллисекунды: UBL-счёт размером около 1 МБ с base64-вложениями валидируется за 15 мс, или 79 мс с XSD UBL 2.1. Запакованный запуск Saxon уходит в CI и в путь отказа, а контейнер покрывает veraPDF. Каким бы ни был встроенный результат, он описывает покрытое подмножество, а не стандарт — ровно об этом напоминает поле coverage
Функции валидации счетов, каталог правил и инструменты пакетов правил поставляются с Delphi PDF Library для Delphi и C++Builder — вместе с API Factur-X, PDF/A-3 и встроенных файлов, использованными выше