חשבונית Factur-X או ZUGFeRD היא למעשה שני מסמכים הלובשים שם קובץ אחד. המסמך החיצוני הוא מכל (container) מסוג PDF/A-3 שקורא ארכיוני חייב לקבל ב-10 השנים הבאות. המסמך הפנימי הוא חשבונית XML שמערכת הנהלת החשבונות של הקונה חייבת לפענח כנגד תקן EN 16931. הטעות שגורמת לשליחת חשבוניות שבורות לייצור (production) היא האמונה שביצוע נכון של הראשון מקבל את השני בחינם. זה לא כך. קובץ יכול להיות PDF/A-3 ללא דופי ועדיין לשאת XML ששום רשות מס לא תקבל, והוא יכול לשאת XML לפי הספר של EN 16931 בתוך מכל שנכשל באימות ארכיוני. שתי השכבות מאומתות על ידי שני כלים שונים שאינם יודעים דבר זה על זה, וצינור עיבוד (pipeline) אמיתי חייב לספק את שניהם
שני מאמתים (validators), שתי שאלות שונות
veraPDF הוא מימוש הייחוס (reference implementation) עבור PDF/A. הפנה אותו לחשבונית והוא עונה על שאלה אחת: האם זהו קובץ PDF/A-3 תואם. הוא בודק את הדברים שמעניינים את תקן ISO 19005-3. האם כל גופן מוטבע (embedded). האם יש OutputIntent. האם מטא-נתוני ה-XMP מצהירים על החלק (part) ורמת התאימות הנכונים. עבור חשבונית אלקטרונית (e-invoice) הוא גם בודק את צנרת הקובץ המשויך (associated-file plumbing) ש-PDF/A-3 דורש, מכיוון שה-XML רוכב כקובץ מוטבע יחד עם /AFRelationship ורשומה במערך ה-/AF של קטלוג המסמך. veraPDF אינו אומר דבר לגבי השאלה האם סך החשבונית מסתכם נכון, משום שזה לא בתחום אחריותו
Mustang הוא מאמת הקוד הפתוח מבית Mustangproject. הוא שואל את השאלה האורתוגונלית: האם ה-XML המוטבע הוא חשבונית תקינה. הוא מריץ את ה-XML מול הסכימה (schema) עבור הפרופיל המוצהר ואז מיישם את הכללים העסקיים של EN 16931 ואת מערכות הכללים הספציפיות למדינה המונחות מעל, ה-CIUS של XRechnung ביניהן. הוא בודק שמזהה המע"מ של המוכר נוכח כאשר הסכומים מחייבים זאת, שסכומי ההנחות (allowance) והחיובים (charge) תואמים לסך המסמך, שה-URN של הפרופיל ב-XML תואם למה שהקובץ מתיימר להיות. ל-Mustang לא אכפת אם ה-PDF העוטף מטביע את הגופנים שלו, מכיוון שזו העבודה של veraPDF
אף כלי אינו קבוצת-על של השני. veraPDF מעביר מכל מושלם מבנית סביב XML חסר היגיון. Mustang מעביר XML מושלם העטוף במכל עם OutputIntent חסר. כל אחד תופס בדיוק את סוג הפגם שהשני עיוור אליו, וזו כל הסיבה לכך שרתמת אימות (validation harness) רצינית מריצה את שניהם ומתייחסת לקובץ ככזה שניתן למשלוח רק כאשר שניהם מסכימים
מטריצת האימות (The validation matrix)
כדי להוכיח שהספרייה מפיקה קבצים ששורדים את שני השערים, רתמת האימות בונה מטריצה. שישה פרופילי חשבונית מכסים את הטווח שצינור עיבוד אירופאי פוגש בפועל: Factur-X EN 16931, Factur-X BASIC, הגרסה Factur-X EXTENDED France B2B, XRechnung 3.0, ZUGFeRD 1.0 COMFORT, ו-ZUGFeRD 2.0 BASIC. כל פרופיל מיוצר כנגד שתי רמות תת-תאימות של PDF/A, 3b ו-3u, משום שדרישות רמה B ורמה U מתפצלות בכל הנוגע למיפוי Unicode וקובץ שעובר אחת עלול להיכשל בשנייה. שישה פרופילים כפול שתי רמות שווה שנים עשר קבצים, כל אחד מהם נבנה באופן "headless" על ידי אותו נתיב קוד שמגיע עם דוגמת ה-GUI, כך שהארטיפקטים הנבדקים אינם מכווננים ידנית עבור הבדיקה
המחולל (generator) כותב את כל השניים-עשר ותסריט מזין כל אחד מהם לשני המאמתים. בריצה המלאה הראשונה veraPDF העביר את כל השניים-עשר. צנרת המכל הייתה נכונה באופן גורף: קבצים משויכים נרשמו, תאימות XMP הוצהרה, output intents במקומם. Mustang העביר שמונה. ארבע חשבוניות היו קבצי PDF/A-3 חוקיים מבנית שנשאו XML שמאמת הכללים העסקיים דחה, שזה בדיוק הפיצול שגישת שני הכלים נועדה להציף. אילו הרתמה הייתה סומכת על veraPDF בלבד, הארבעה הללו היו נראים גמורים
שני התיקונים שסגרו את הפער
ארבעת הכישלונות ב-Mustang נבעו משני גורמים נפרדים, והתיקון לכל אחד מהם הוא פרט ששווה לדעת לפני שאתה מחולל את הפרופילים הללו בעצמך
הראשון היה פרופיל Factur-X EXTENDED France B2B. המחולל המקורי העביר תווית פנימית כרמת התאימות ו-URN פנימי כקווים המנחים (guideline), ו-Mustang דחה את הקובץ עם שגיאת ערך-תאימות-לא-חוקי (invalid-conformance-value) ולאחריה שגיאת סוג-פרופיל-לא-נתמך (unsupported-profile-type). הסיבה לכך היא ששדה ה-XMP fx:ConformanceLevel אינו משבצת טקסט חופשי עבור מתן שמות לפרופיל משלך. Factur-X מגדיר בדיוק חמישה ערכים סטנדרטיים עבורו: MINIMUM, BASIC WL, BASIC, EN 16931, ו-EXTENDED. חשבונית B2B ספציפית לצרפת היא עדיין מסמך בפרופיל EXTENDED בכל הנוגע למטא-נתוני ה-XMP. האופי הצרפתי של החשבונית אינו בא לידי ביטוי בהמצאת ערך תאימות שישי. הוא בא לידי ביטוי בקוד המדינה, FR, ובמזהה ה-guideline בתוך ה-XML, שחייב לשאת את הקידומת urn:cen.eu:en16931:2017#conformant# המסמנת CIUS שתואם ל-EN 16931. העברת ערך ה-EXTENDED הסטנדרטי עם FR כקוד המדינה ו-URN ה-guideline הנכון הפכו את הקובץ לתואם
ב-API של הספרייה זוהי קריאה ל-AddFacturXAssociatedFileFromString כאשר התאימות, המדינה וה-guideline מיושרים (aligned). ארגומנט רמת התאימות נושא את האסימון (token) הסטנדרטי, ארגומנט קוד המדינה נושא את FR, וה-URN של ה-guideline חי בבתי ה-XML שאתה מעביר פנימה
var
FileID: Integer;
begin
PDF.SetPDFAMode(5); // PDF/A-3b
PDF.NewDocument;
// ... draw the human-readable invoice page ...
// ExtendedXML carries an EN 16931 guideline URN of the form
// urn:cen.eu:en16931:2017#conformant#urn:factur-x.eu:1p0:extended
FileID := PDF.AddFacturXAssociatedFileFromString(
ExtendedXML,
'EXTENDED', // standard fx:ConformanceLevel, not an internal label
'factur-x.xml',
'Factur-X EXTENDED invoice',
'Alternative', // /AFRelationship
'1.0',
'FR'); // France B2B marked by country code, not by conformance
if FileID = 0 then
raise Exception.Create('Factur-X attachment rejected');
PDF.SaveToFile('02_Factur-X-EXTENDED-FR_PDFA-3b.pdf');
end;
הגורם השני היה פרופיל ZUGFeRD 1.0 COMFORT, ולא היה לו שום קשר למטא-נתונים. ZUGFeRD 1.0 מאומת כנגד ה-XSD של :1p0, שהוא מחמיר יותר לגבי עוצמה (cardinality) ממה שתקצירי המלל מרמזים. ה-XSD דורש שסיכום ההסדר של הכותרת, ram:SpecifiedTradeSettlementMonetarySummation, יכיל את ram:ChargeTotalAmount ואת ram:AllowanceTotalAmount כל אחד בדיוק פעם אחת. ה-XML שחולל השמיט את שניהם, לכן Mustang דיווח שהאלמנטים חייבים להופיע בדיוק פעם אחת. אלו אינם אופציונליים כאשר הסכימה אומרת ש-minOccurs הוא אחד. פליטת (emitting) שניהם בסדר רצף ה-XSD, מיד לאחר ram:LineTotalAmount, עם ערך של 0.00 כאשר אין חיובים או הנחות, סיפקה את הסכימה. אפס הוא אלמנט נוכח; אלמנט חסר הוא הפרה של הסכימה. כששני התיקונים הללו במקומם, המטריצה עלתה לשניים עשר מתוך שניים עשר ב-Mustang תוך שהיא נשארת שנים עשר מתוך שנים עשר ב-veraPDF
השדות ב-XRechnung שהופכים "לא תקין" ל"תקין"
XRechnung ראוי להערה משלו משום שה-CIUS הגרמני שלו מוסיף כללים עסקיים שנעדרים מקבוצת הבסיס של EN 16931, והם נכשלים בדרכים שנראות במבט ראשון כאילו שום דבר אינו פגום במסמך. שניים מהם נוגעים לכתובות אלקטרוניות. BT-34 היא הכתובת האלקטרונית של המוכר ו-BT-49 היא הכתובת האלקטרונית של הקונה, נקודות קצה (endpoints) הניתוב שפורטל המגזר הציבורי הגרמני משתמש בהן כדי למסור ולאשר את החשבונית. מודל הבסיס של EN 16931 מתייחס אליהם כאופציונליים. XRechnung אינו עושה זאת. השמט כל אחד מהם והחשבונית תהיה בנויה כהלכה (well-formed), חוקית מבחינת סכימה - ותידחה
השלישי הוא כלל BR-DE-6, אשר דורש שמספר הטלפון ליצירת קשר עם המוכר יהיה נוכח. זה סוג השדה שמפתח משמיט כי זה מרגיש כמו תצוגה (presentation) ולא נתונים (data), והיעדרו מייצר כישלון אימות שמצביע על קבוצת איש הקשר של המוכר במקום על משהו שברור שחסר. אספקת BT-34, BT-49, ומספר הטלפון של המוכר היא מה שמעביר קובץ XRechnung ממצב לא-חוקי (invalid) לחוקי (valid) תחת Mustang, ושום דבר מזה לא משנה שום דבר ש-veraPDF רואה, מכיוון ששלושתם חיים ב-XML
חיווט (Wiring) הפלט של הספרייה אל מאמת
הנקודה הארכיטקטונית שמאחורי הרתמה ניתנת להכללה לכל מערכת עסקית. ספריית ה-PDF כותבת מכל תואם (conformant container) ומטביעה את ה-XML. היא אינה, ולא צריכה, לנסות להיות הסמכות של הכללים העסקיים של EN 16931. ValidateFacturXInvoice בספרייה בודקת את עקביות המכל, שמערך ה-/AF של הקטלוג, עץ השמות של הקבצים המוטבעים, DocumentFileName ב-XMP, הפרופיל, ה-guideline, ו-/AFRelationship כולם מסכימים, אך היא אינה מאמתת קודי מס או מתאמת סכומים. חלוקת העבודה הנכונה היא שהמערכת העסקית תחלץ את ה-XML ותמסור אותו למאמת חשבוניות ייעודי, בדיוק כפי שהרתמה מוסרת אותו ל-Mustang
קריאת הקובץ בחזרה אומרת לך מה נכתב בפועל. DetectFacturXInvoice מדווחת האם זוהתה חשבונית, ו-GetFacturXInvoiceInfo קוראת את שדות המטא-נתונים לפי תג (tag): תג 1 הוא שם הקובץ המוטבע, תג 2 הוא ה-DocumentFileName ב-XMP, תג 5 הוא רמת התאימות, תג 6 הוא מזהה ה-guideline, ותג 7 הוא /AFRelationship. וידוא שרמת התאימות שאתה קורא בחזרה היא האסימון (token) הסטנדרטי ולא תווית פנימית היא הדרך הזולה ביותר לתפוס את השגיאה של EXTENDED לפני שקובץ עוזב את תהליך הבנייה (build) שלך
function ExtractAndInspect(const PdfPath: string): AnsiString;
var
Profile, Guideline: WideString;
begin
Result := '';
PDF.LoadFromFile(PdfPath);
if PDF.DetectFacturXInvoice = 1 then
begin
Profile := PDF.GetFacturXInvoiceInfo(5); // fx:ConformanceLevel
Guideline := PDF.GetFacturXInvoiceInfo(6); // XML guideline ID
Writeln('Profile: ', Profile);
Writeln('Guideline: ', Guideline);
// Hand the raw XML to a dedicated EN 16931 / Mustang validator.
Result := PDF.ExtractFacturXXMLToString;
end;
end;
ExtractFacturXXMLToString מחזירה את בתי ה-XML הגולמיים כ-AnsiString, מוכנים לכתיבה לקובץ או להזרמה (streaming) לתוך תהליך מאמת (validator process). ברתמת הבדיקה היעד הזה הוא Mustang, שמופעל דרך ה-jar של שורת הפקודה שלו, כש-veraPDF רץ באותו מעבר (pass) על אותו קובץ. החיווט קטן: מחולל מסוף (console generator), EInvoiceValidation.dpr, כותב את שנים עשר הקבצים בעזרת מודל החשבונית המשותף מהדוגמה, ותסריט, run-validation.ps1, מניע את שני המאמתים על פני ספריית הפלט ומדפיס טבלת מעבר (pass) וכישלון (fail). צורה דו-שלבית זו, חולל בעזרת הספרייה ואמת עם מאמתים חיצוניים, היא מה שמשימת אינטגרציה רציפה (CI) צריכה להריץ בכל שינוי ביצירת חשבוניות, משום שהדרך היחידה לדעת שקובץ מספק את שתי השכבות היא לשאול את שני הכלים
אם הצינור שלך גם צריך להסמיך (certify) את המכל לפני חתימה, הצד של טרום-הטיסה (preflight) בעבודה זו מכוסה בסקירה שלנו של preflight עבור PDF/A ו-PDF/UA ב-Delphi, וזרימת ה-certify-then-sign הרחבה יותר מתוארת בסביבת העבודה (workbench) של תאימות וחתימה. שניהם נבנים על אותו נתיב יצירה שמסופק כחלק מ-Delphi PDF Library עבור Delphi ו-C++Builder, לצד ה-PDF/A, ה-associated-file, וממשקי ה-API של מטא-נתונים המשמשים כאן