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

Хибридни фактури Factur-X и ZUGFeRD в Delphi

Една съответстваща електронна фактура не е PDF с прикачен отстрани XML файл. Тя е единичен документ PDF/A-3, който носи фактурата два пъти: веднъж като страница, която човек чете, и веднъж като машинно четим Cross Industry Invoice XML, съхранен вътре във файла като асоцииран файл. Двете представяния описват една и съща фактура. Тази двойствена природа е основният смисъл на семействата формати, които европейските мандати вече изискват – Factur-X във Франция и Германия, ZUGFeRD в немскоговорящите пазари и XRechnung за фактуриране в публичния сектор в Германия. Тази статия разглежда как PDF Library for Delphi сглобява такава хибридна фактура в Delphi, къде стандартите оставят място за грешки и защо един профил в каталога се нуждае от напълно отделен XML строител (builder)

Какво всъщност е хибридна фактура

Видимата страница и вграденият XML обслужват различни читатели. Служител, който одобрява плащане, гледа рендерираната страница. Система за задължения (accounts-payable) поглъща 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 дефинира потока на вградения файл (embedded file stream), обекта, който държи суровия XML и неговите параметри. Частта, която прави файла валиден асоцииран файл (associated file), е §14.13, която добавя концепцията за асоцииран файл и ключа /AFRelationship. Този ключ указва как вградените данни се свързват със съдържанието, към което са прикрепени, и стойността, която Factur-X изисква, е Alternative

Изборът има значение, защото другите стойности биха твърдели нещо невярно за документа. Source би означавало, че XML-ът е материалът, от който е генерирано видимото съдържание – оригинал, от който произлиза страницата. Supplement би означавало, че XML-ът добавя информация отвъд това, което страницата показва, допълнение, което не се съдържа в рендерирането. Нито едно от двете не е това, което е фактурата Factur-X. XML-ът и страницата са два еквивалентни израза на една фактура, носещи едно и също правно съдържание в две форми. Alternative е стойността, която казва точно това: еквивалентно алтернативно представяне на видимото съдържание. Валидатор, който прочете каквато и да е друга връзка във файл Factur-X, ще го отхвърли и с право, защото връзката е машинно четимо твърдение за това какво представлява прикаченият файл

Анатомия на хибридна фактура Factur-X, изградена с PDF Library for Delphi: един документ PDF/A-3 държи четивната страница и вграден factur-x.xml, съединени чрез AFRelationship Alternative под модела EN 16931
Един PDF/A-3 файл носи фактурата два пъти като еквивалентни представления, и само /AFRelationship = Alternative изразява това твърдение по начин, който валидаторите приемат

Каталогът с профили

Примерът за електронна фактура (E-Invoice sample), който се доставя с PDF Library for Delphi, задвижва един и същ път на генериране през шест профила, дефинирани като масив от записи (records) в InvoiceModel.pas. Всеки профил носи стойностите, от които се нуждае писателят (writer): екранно име, име на вградения файл, ниво на съответствие (conformance level), /AFRelationship, версия, незадължителен код на държавата и GuidelineID URN, който 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 с версия на пространството от имена (namespace version) :100, чийто корен (root element) е rsm:CrossIndustryInvoice. ZUGFeRD 1.0 предшества тази схема. Той е 2014 CrossIndustryDocument с версия на пространството от имена :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 означава наследената схема ZUGFeRD 1.0 :1p0; всеки
  // друг профил е модерната 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 има три нива на съответствие и PDF Library for Delphi ги избира чрез SetPDFAMode. Режим 5 е PDF/A-3b, нивото, което гарантира надеждно визуално възпроизвеждане. Режим 6 е PDF/A-3a, което добавя изискванията за тагната структура и достъпност (accessibility) на ниво a. Режим 7 е PDF/A-3u, което изисква целият текст да бъде мапнат към Unicode. Активирането на режима също вгражда вградения в библиотеката sRGB output intent – цветовото характеризиране, което PDF/A изисква, така че рендерираният цвят да бъде дефиниран, а не зависим от устройството

Повечето потоци на фактури работят на 3b, което е достатъчно за вярна видима страница плюс вградения XML. Ако имате нужда от изричен ICC профил, а не от вградения, LoadOutputIntentProfile го подменя след като е зададен режимът. Примерът зарежда sRGB профила от хранилището по този начин и се връща към вградения intent, когато файлът е недостъпен, така че output intent-ът винаги присъства

PDF := TPDFlib.Create;
try
  // Режим 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');

  // По избор: замяна на вградения sRGB intent с изричен ICC профил.
  if PDF.LoadOutputIntentProfile(ICCFile, 'DeviceRGB') <> 1 then
    { преминава към вградения sRGB intent, който SetPDFAMode е вградил };
finally
  // ... продължи изграждането на документа
end;

Изграждане на хибридната фактура

С конфигуриран контейнер останалото са три стъпки по ред: задайте режим PDF/A-3, изчертайте четимата от човек страница, след това прикачете XML като асоцииран файл. Видимата страница е обикновено съдържание. Единственото ограничение, което си струва да запомните, е, че PDF/A забранява невградените Standard 14 шрифтове, така че страницата трябва да вгради истински шрифт, вместо да реферира към вграден такъв

Прикачването е едно извикване. AddFacturXAssociatedFileFromString приема суровите UTF-8 XML байтове плюс метаданните на профила, записва потока на вградения файл, регистрира го в масива /AF на Catalog-а, който PDF/A-3 изисква, прилага /AFRelationship и генерира XMP метаданните на електронната фактура, които идентифицират документа като Factur-X, ZUGFeRD или XRechnung. Също така проверява дали guideline ID в XML съвпада с нивото на съответствие, което сте поискали, така че несъответствието между XML-а, който сте изградили, и профила, който сте посочили, се улавя, вместо безшумно да се изпрати

// 1. Режимът PDF/A-3 и output intent-ът вече са зададени.
// 2. Изчертай видимата страница (вгражда истински TrueType шрифт).
DrawInvoicePage(PDF, AProfile, Data);

// 3. Изгради коректния за профила XML и го прикачи като
//    асоцииран файл с /AFRelationship = Alternative.
InvoiceXML := BuildInvoiceXML(AProfile, Data);   // AnsiString от UTF-8 байтове
FileID := PDF.AddFacturXAssociatedFileFromString(
  InvoiceXML,
  AProfile.ConformanceLevel,   // напр. 'EN16931'
  AProfile.FileName,           // 'factur-x.xml'
  AProfile.Description,
  AProfile.Relationship,       // 'Alternative'
  AProfile.Version,            // '1.0'
  AProfile.CountryCode);       // '' или 'DE' или '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 октети. Обикновен cast през системната ANSI кодова страница би повредил тези знаци и тихо би произвел фактура, чийто XML вече не съвпада със собствената ѝ декларация. Примерът кодира в UTF-8 изрично, преди да предаде байтовете, което е безопасният начин за захранване на всяко байт-ориентирано PDF API от Unicode string

Диаграма на каталога с профили на PDF Library for Delphi, насочващ шест профила за е-фактури Factur-X, ZUGFeRD и XRechnung през BuildInvoiceXMLText към модерното семейство UN/CEFACT Cross Industry Invoice и остарялото семейство ZUGFeRD 1.0 :1p0
Шестте профила споделят една точка за разпределяне, но ZUGFeRD 1.0 COMFORT се отклонява към наследената схема :1p0 и се нуждае от собствен XML builder

За прикачване на XML, който не е разпознат профил на електронна фактура, AddPDFA3AssociatedFileFromString е генеричният еквивалент. Той приема име на файл, MIME тип, описание, връзка и байтове и записва обикновен PDF/A-3 асоцииран файл без никакви специфични за фактури метаданни или проверки на ръководства (guideline checks). Използвайте го за допълнителни данни; използвайте метода Factur-X за фактури, така че метаданните на профила и съвпадението на ръководството да бъдат записани вместо вас

След като документът е произведен, следващите въпроси са дали преминава валидация за PDF/A и достъпност, и дали може да бъде подписан, без да се нарушава съответствието. Те са обхванати в нашето ръководство за PDF/A и PDF/UA preflight в Delphi и работната среда за съответствие и подписване. Всичко това се доставя като част от PDF Library for Delphi Delphi PDF Library, заедно с API-тата за PDF/A, тагване и свойства на документа, върху които надгражда пътят на електронната фактура

Диаграма на конвейера за изграждане на хибридна фактура Factur-X или ZUGFeRD с PDF Library for Delphi: SetPDFAMode конфигурира контейнера PDF/A-3, видимата страница се чертае, AddFacturXAssociatedFileFromString прикачва фактурното XML, после SaveToFile публикува архивния документ
Сглобяването протича в три подредени стъпки преди записване, като нивата на съответствие, безопасната за байтове обработка на UTF-8 и универсалният API за свързани файлове остават странични бележки