חשבונית אלקטרונית תקנית אינה 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. שני הפורמטים עונים למאמתים ומוסכמות שונים, והדוגמה קובעת את היחס של כל פרופיל מהקטלוג במקום לקודד-קשיח ערך יחיד, מה שהסיבה שהשדה per-פרופיל קיים במקום קבוע
מלכודת ZUGFeRD 1.0
מפתה להניח שכל פרופיל הוא Cross Industry Invoice של EN 16931 עם שונות מינורית בכמה שדות אופציונליים שאתה מאכלס. זה תקף לחמישה מתוך השישה. זה לא תקף ל-ZUGFeRD 1.0 COMFORT, והסיבה מבנית ולא קוסמטית
הפרופילים המודרניים פולטים UN/CEFACT Cross Industry Invoice עם גרסת namespace :100, שאלמנט השורש שלו הוא rsm:CrossIndustryInvoice. ZUGFeRD 1.0 קודם לסכמה ההיא. הוא ה-CrossIndustryDocument מ-2014 עם גרסת namespace :1p0, ואלמנט השורש שלו הוא rsm:CrossIndustryDocument. ה-URN של ה-namespace שונים, אלמנט השורש שונה, ועץ האלמנטים שונה לאורך כולו: סכמת :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 של המאגר בדרך זו ונסוגה ל-intent המובנה כאשר הקובץ אינו נגיש, כך ש-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 הגולמיים בתוספת המטא-נתונים של הפרופיל, כותב את זרם הקובץ המשובץ, רושם אותו במערך /AF של ה-Catalog ש-PDF/A-3 דורש, מחיל את /AFRelationship, ומייצר את מטא-נתוני ה-e-invoice ב-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 חייב להגיע לקריאה כ-octets UTF-8 גולמיים. המרה פשוטה דרך עמוד-הקוד של ה-ANSI של המערכת היתה משחיתה את התווים האלה ומייצרת בשקט חשבונית ש-XML שלה כבר לא תואם את ההכרזה של עצמו. הדוגמה מקודדת ל-UTF-8 במפורש לפני מסירת הבתים, שזו הדרך הבטוחה להזין כל ממשק PDF מונחה-בתים מ-string מבוסס Unicode
לצירוף XML שאינו פרופיל חשבונית-אלקטרונית מוכר, AddPDFA3AssociatedFileFromString הוא המקבילה הגנרית. היא לוקחת שם קובץ, סוג MIME, תיאור, יחס, ובתים, וכותבת קובץ משויך PDF/A-3 רגיל בלי מטא-נתוני חשבונית או בדיקות guideline. השתמש בה לנתונים משלימים; השתמש במתודת Factur-X לחשבוניות, כדי שמטא-נתוני הפרופיל והתאמת ה-guideline ייכתבו עבורך
לאחר שהמסמך מיוצר, השאלות הבאות הן האם הוא עובר אימות PDF/A ונגישות, והאם ניתן לחתום עליו מבלי לשבור התאמה. אלה מכוסים ב-הליכי העברה של preflight PDF/A ו-PDF/UA וב-workbench ההתאמה והחתימה. כל זה מופץ כחלק מ-PDFlibPas Delphi PDF Library, לצד ממשקי ה-API של PDF/A, תיוג, ומאפייני-מסמך שנתיב ה-e-invoice נבנה עליהם