Technical Article

Гібридні рахунки Factur-X та ZUGFeRD у Delphi

Відповідний електронний рахунок - це не 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, версію, необов'язковий код країни та URN 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

Існує спокуса припустити, що кожен профіль є Cross Industry Invoice EN 16931 з незначними варіаціями в тому, скільки необов'язкових полів ви заповнюєте. Це стосується п'яти з шести профілів. Але це не стосується ZUGFeRD 1.0 COMFORT, і причина є структурною, а не косметичною

Сучасні профілі генерують UN/CEFACT Cross Industry Invoice з версією простору імен :100, кореневим елементом якого є rsm:CrossIndustryInvoice. ZUGFeRD 1.0 передує цій схемі. Це CrossIndustryDocument 2014 року з версією простору імен :1p0, і його кореневим елементом є rsm:CrossIndustryDocument. URN простору імен відрізняються, кореневий елемент відрізняється, і дерево елементів повністю відрізняється: схема :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 стандартних шрифтів (Standard 14), тому на сторінці має бути вбудований реальний шрифт, а не посилання на вбудований

Прикріплення - це один виклик. AddFacturXAssociatedFileFromString приймає необроблені байти XML у кодуванні UTF-8 разом з метаданими профілю, записує потік вбудованого файлу, реєструє його в масиві /AF каталогу, який вимагає PDF/A-3, застосовує /AFRelationship і генерує метадані е-рахунку XMP, що ідентифікують документ як Factur-X, ZUGFeRD або XRechnung. Він також перевіряє, чи ідентифікатор вказівки 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 перед передачею байтів, що є безпечним способом подачі будь-якого байт-орієнтованого PDF API з рядка Unicode string

Для прикріплення XML, який не є розпізнаним профілем е-рахунку, універсальним аналогом є AddPDFA3AssociatedFileFromString. Він приймає ім'я файлу, тип MIME, опис, зв'язок і байти та записує звичайний пов'язаний файл PDF/A-3 без будь-яких специфічних для рахунку метаданих або перевірок вказівок. Використовуйте його для додаткових даних; використовуйте метод Factur-X для рахунків, щоб метадані профілю та відповідність вказівкам були записані за вас

Коли документ створено, наступні питання полягають у тому, чи пройде він перевірку PDF/A і доступності, і чи можна його підписати без порушення відповідності. Це описано в нашому посібнику з попередньої перевірки PDF/A і PDF/UA у Delphi та робочому середовищі відповідності та підписання. Все це постачається як частина PDFlibPas Delphi PDF Library, поряд із API для PDF/A, тегів та властивостей документа, на яких базується шлях е-рахунку