Соответствующий электронный счёт - это не PDF с прикреплённым сбоку XML-файлом. Это единый документ PDF/A-3, который содержит счёт дважды: один раз в виде страницы, которую читает человек, и один раз в виде машиночитаемого XML Cross Industry Invoice, хранящегося внутри файла как связанный файл. Два представления описывают один и тот же счёт. Эта двойственная природа и является смыслом семейств форматов, которые теперь требуют европейские мандаты: Factur-X во Франции и Германии, ZUGFeRD на немецкоязычных рынках и XRechnung для выставления счетов в государственном секторе Германии. В этой статье описывается, как PDFlibPas создаёт такой гибридный счёт в Delphi, где стандарты оставляют место для ошибок и почему один профиль в каталоге требует совершенно отдельного построителя XML
Что такое гибридный счёт на самом деле
Видимая страница и встроенный XML служат разным читателям. Клерк, утверждающий платёж, смотрит на отображаемую страницу. Система учёта кредиторской задолженности принимает XML, считывает итоги и разбивку по налогам в виде структурированных полей и проводит запись без ручного ввода данных человеком. Семантическое содержание этого XML регулируется EN 16931, европейским стандартом, определяющим модель данных счёта: какие поля существуют, что они означают и какие из них обязательны. EN 16931 - это семантическая модель, а не формат файла. Factur-X, ZUGFeRD 2.x и XRechnung - все реализуют эту модель в виде документа UN/CEFACT Cross Industry Invoice, синтаксиса, который передаёт поля EN 16931 по каналу связи
Для того чтобы документ был архивируемым и самоописывающимся, контейнер является PDF/A-3, определённым стандартом ISO 19005-3. PDF/A-3 - это уровень соответствия, разрешающий произвольные встроенные файлы, что именно и нужно XML счёта. PDF/A-2 запрещает встраивание файлов, которые сами не являются PDF/A, поэтому счёт Factur-X не может быть PDF/A-2. Выбор PDF/A-3 поэтому не является предпочтением - это требование, непосредственно следующее из желания встроить данные не в формате PDF в архивный документ
Почему отношение является Alternative
Встраивание байтов - это простая часть. ISO 32000 §7.11.4 определяет поток встроенного файла, объект, содержащий необработанный XML и его параметры. Частью, которая делает файл допустимым связанным файлом, является §14.13, который добавляет концепцию связанного файла и ключ /AFRelationship. Этот ключ указывает, как встроенные данные связаны с содержимым, к которому они прикреплены, а значение, которое требует Factur-X, - Alternative
Выбор важен, потому что другие значения утверждали бы что-то ложное о документе. Source означало бы, что XML является материалом, из которого было сгенерировано видимое содержимое, мастером, от которого страница является производной. Supplement означало бы, что XML добавляет информацию, помимо того, что отображает страница, дополнение, не содержащееся в отображении. Ни то, ни другое не соответствует тому, чем является счёт Factur-X. XML и страница - это два равнозначных выражения одного счёта, несущих одно и то же юридическое содержание в двух формах. Alternative - это значение, которое говорит именно это: равнозначное альтернативное представление видимого содержимого. Валидатор, который обнаружит любое другое отношение в файле Factur-X, отклонит его, и справедливо, поскольку отношение является машиночитаемым утверждением о том, для чего предназначено вложение
Каталог профилей
Образец E-Invoice, поставляемый с PDFlibPas, запускает один и тот же путь генерации по шести профилям, определённым как массив записей в InvoiceModel.pas. Каждый профиль несёт значения, необходимые записывающей программе: отображаемое имя, имя встроенного файла, уровень соответствия, /AFRelationship, версию, необязательный код страны и URI GuidelineID, который XML объявляет внутри своего контекста документа
Шесть профилей - Factur-X EN16931, Factur-X BASIC, Factur-X EXTENDED для Франции, XRechnung 3.0, ZUGFeRD 1.0 COMFORT и ZUGFeRD 2.0 BASIC. GuidelineID - это поле, которое точно сообщает получателю, какой профиль ожидать, и значения специфичны. Factur-X EN16931 объявляет urn:cen.eu:en16931:2017. XRechnung 3.0 объявляет urn:cen.eu:en16931:2017#compliant#urn:xeinkauf.de:kosit:xrechnung_3.0. ZUGFeRD 2.0 BASIC объявляет urn:cen.eu:en16931:2017#compliant#urn:zugferd.de:2p0:basic. Имя встроенного файла тоже является частью контракта. Профили Factur-X встраивают factur-x.xml, XRechnung встраивает xrechnung.xml, а профили ZUGFeRD встраивают ZUGFeRD-invoice.xml или zugferd-invoice.xml. Получатель сканирует имена вложений, чтобы найти счёт, поэтому имя файла не является декоративным
Одна деталь в каталоге заслуживает внимательного прочтения. Большинство профилей используют отношение Alternative, но запись XRechnung 3.0 в образце использует Source. Два формата подчиняются разным валидаторам и соглашениям, и образец устанавливает отношение каждого профиля из каталога, а не жёстко кодирует единственное значение, что и объясняет существование поля для каждого профиля, а не константы
Ловушка ZUGFeRD 1.0
Велик соблазн предположить, что каждый профиль - это EN 16931 Cross Industry Invoice с незначительными вариациями в том, сколько необязательных полей вы заполняете. Это верно для пяти из шести. Для ZUGFeRD 1.0 COMFORT это не так, и причина носит структурный, а не косметический характер
Современные профили генерируют UN/CEFACT Cross Industry Invoice с версией пространства имён :100, корневой элемент которого - rsm:CrossIndustryInvoice. ZUGFeRD 1.0 предшествует этой схеме. Это CrossIndustryDocument 2014 года с версией пространства имён :1p0, корневой элемент которого - rsm:CrossIndustryDocument. URI пространств имён различаются, корневой элемент различается, и дерево элементов различается повсюду: схема :1p0 группирует данные под ApplicableSupplyChainTradeAgreement, ApplicableSupplyChainTradeDelivery и ApplicableSupplyChainTradeSettlement, тогда как :100 использует ApplicableHeaderTradeAgreement, ApplicableHeaderTradeDelivery и ApplicableHeaderTradeSettlement. Именование достаточно похоже, чтобы ввести в заблуждение, и достаточно различается, чтобы сломать работу
Слово COMFORT в названии профиля описывает насыщенность данных - профиль уровня автоматизации с полными позициями, разбивкой по налогам и условиями оплаты, - а не то, какая схема его несёт. Поэтому нельзя взять документ :100 и переклеить его под ZUGFeRD 1.0. Образец обрабатывает это с помощью флага в каждой записи профиля и двух отдельных функций-строителей, выбирая правильную до начала генерации какого-либо XML
function BuildInvoiceXMLText(const AProfile: TeInvoiceProfile;
const Data: TInvoiceData): string;
begin
// XMLFamily = 1 means the legacy ZUGFeRD 1.0 :1p0 schema; every
// other profile is the modern UN/CEFACT :100 Cross Industry Invoice.
if AProfile.XMLFamily = 1 then
Result := BuildZUGFeRD1Text(AProfile, Data)
else
Result := BuildCII100Text(AProfile, Data);
end;
Разделение - это не реализационная любезность. Подача дерева :100 получателю ZUGFeRD 1.0 создаёт документ, который не проходит валидацию схемы на корневом элементе, поэтому два семейства должны создаваться кодом, который знает, что именно он записывает
Выбор уровня PDF/A-3
PDF/A-3 имеет три уровня соответствия, и PDFlibPas выбирает их через SetPDFAMode. Режим 5 - это PDF/A-3b, уровень, гарантирующий надёжное визуальное воспроизведение. Режим 6 - это PDF/A-3a, который добавляет требования к теговой структуре и доступности уровня a. Режим 7 - это PDF/A-3u, который требует, чтобы весь текст был привязан к Unicode. Включение режима также встраивает встроенный цветовой профиль sRGB библиотеки, цветовую характеристику, которую PDF/A требует, чтобы отображаемый цвет был определён, а не зависел от устройства
Большинство процессов выставления счетов работают на уровне 3b, которого достаточно для достоверной видимой страницы плюс встроенный XML. Если вам нужен явный профиль ICC вместо встроенного, LoadOutputIntentProfile заменяет его после установки режима. Образец загружает профиль sRGB из репозитория таким образом и возвращается к встроенному намерению, когда файл недоступен, так что намерение вывода всегда присутствует
PDF := TPDFlib.Create;
try
// Mode 5 = PDF/A-3b, 6 = PDF/A-3a, 7 = PDF/A-3u.
if PDF.SetPDFAMode(5) <> 1 then
raise Exception.Create('PDF/A-3 mode could not be enabled');
// Optional: swap the built-in sRGB intent for an explicit ICC profile.
if PDF.LoadOutputIntentProfile(ICCFile, 'DeviceRGB') <> 1 then
{ fall back to the built-in sRGB intent that SetPDFAMode embedded };
finally
// ... continue building the document
end;
Создание гибридного счёта
После настройки контейнера всё остальное состоит из трёх шагов по порядку: установите режим PDF/A-3, нарисуйте удобочитаемую для человека страницу, затем прикрепите XML в качестве связанного файла. Видимая страница - это обычное содержимое. Единственное ограничение, о котором стоит помнить, заключается в том, что PDF/A запрещает невстроенные стандартные 14 шрифтов, поэтому страница должна встраивать реальное начертание шрифта, а не ссылаться на встроенный
Вложение - это единственный вызов. AddFacturXAssociatedFileFromString принимает необработанные байты XML в кодировке UTF-8 плюс метаданные профиля, записывает поток встроенного файла, регистрирует его в массиве /AF каталога, который требует PDF/A-3, применяет /AFRelationship и генерирует метаданные XMP электронного счёта, идентифицирующие документ как Factur-X, ZUGFeRD или XRechnung. Он также проверяет, что GuidelineID XML соответствует запрошенному уровню соответствия, так что несоответствие между созданным XML и названным профилем выявляется, а не отправляется молча
// 1. PDF/A-3 mode and output intent are already set.
// 2. Draw the visible page (embeds a real TrueType font).
DrawInvoicePage(PDF, AProfile, Data);
// 3. Build the profile-correct XML and attach it as an
// associated file with /AFRelationship = Alternative.
InvoiceXML := BuildInvoiceXML(AProfile, Data); // AnsiString of UTF-8 bytes
FileID := PDF.AddFacturXAssociatedFileFromString(
InvoiceXML,
AProfile.ConformanceLevel, // e.g. 'EN16931'
AProfile.FileName, // 'factur-x.xml'
AProfile.Description,
AProfile.Relationship, // 'Alternative'
AProfile.Version, // '1.0'
AProfile.CountryCode); // '' or 'DE' or 'FR'
if FileID <= 0 then
raise Exception.Create('Invoice XML could not be attached');
PDF.SaveToFile(TargetFile);
Одна тонкость в пути данных касается кодировки. Встроенный XML объявляет encoding="UTF-8", а метод принимает байты в виде AnsiString, поэтому имя продавца или покупателя, содержащее символы не из ASCII, должно поступить на вызов в виде необработанных октетов UTF-8. Прямое преобразование через системовую кодовую страницу ANSI повредило бы эти символы и незаметно создало бы счёт, XML которого больше не соответствует своему собственному объявлению. Образец явно кодирует в UTF-8 перед передачей байтов, что является безопасным способом подачи данных в любой побайтовый API PDF из строки Unicode
Для прикрепления XML, не являющегося признанным профилем электронного счёта, AddPDFA3AssociatedFileFromString является универсальным аналогом. Он принимает имя файла, тип MIME, описание, отношение и байты и записывает простой связанный файл PDF/A-3 без каких-либо метаданных, специфичных для счёта, или проверок руководящего принципа. Используйте его для дополнительных данных; используйте метод Factur-X для счетов, чтобы метаданные профиля и соответствие руководящему принципу были записаны за вас
После создания документа следующие вопросы: проходит ли он валидацию PDF/A и доступности, и может ли он быть подписан без нарушения соответствия. Это описано в руководстве по предпечатной проверке PDF/A и PDF/UA и стенде соответствия и подписи. Всё это поставляется как часть PDFlibPas Delphi PDF Library вместе с API PDF/A, тегирования и свойств документа, на которых строится путь электронного счёта