Technical Article

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

Една съответстваща електронна фактура не е PDF с прикачен отстрани XML файл. Тя е единичен документ PDF/A-3, който носи фактурата два пъти: веднъж като страница, която човек чете, и веднъж като машинно четим Cross Industry Invoice XML, съхранен вътре във файла като асоцииран файл. Двете представяния описват една и съща фактура. Тази двойствена природа е основният смисъл на семействата формати, които европейските мандати вече изискват – Factur-X във Франция и Германия, ZUGFeRD в немскоговорящите пазари и XRechnung за фактуриране в публичния сектор в Германия. Тази статия разглежда как PDFlibPas сглобява такава хибридна фактура в 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, ще го отхвърли и с право, защото връзката е машинно четимо твърдение за това какво представлява прикаченият файл

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

Примерът за електронна фактура (E-Invoice sample), който се доставя с PDFlibPas, задвижва един и същ път на генериране през шест профила, дефинирани като масив от записи (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 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, което добавя изискванията за тагната структура и достъпност (accessibility) на ниво a. Режим 7 е PDF/A-3u, което изисква целият текст да бъде мапнат към Unicode. Активирането на режима също вгражда вградения в библиотеката sRGB output intent – цветовото характеризиране, което PDF/A изисква, така че рендерираният цвят да бъде дефиниран, а не зависим от устройството

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

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 забранява невградените 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 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 октети. Обикновен cast през системната ANSI кодова страница би повредил тези знаци и тихо би произвел фактура, чийто XML вече не съвпада със собствената ѝ декларация. Примерът кодира в UTF-8 изрично, преди да предаде байтовете, което е безопасният начин за захранване на всяко байт-ориентирано PDF API от Unicode string

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

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