מאמר טכני

חשבוניות דיגיטליות מסוג ZUGFeRD ו-Factur-X ב-Delphi עם HotPDF

רכיב HotPDF מייצר, מזהה ומאמת חשבוניות אלקטרוניות מסוג ZUGFeRD ו-Factur-X מתוך Delphi ו-C++Builder. הפונקציה AddFacturXAssociatedFile מטמיעה את קובץ ה-XML של החשבונית לפי תקן EN 16931 לתוך מכל PDF/A-3, הפונקציה DetectFacturXInvoice מזהה חשבונית היברידית נכנסת, והפונקציה HPDFValidateEInvoice מריצה את כללי העסק הרשמיים של Schematron EN 16931 מול ה-XML המוטמע. מאמר זה עובר על כל שלושת השלבים ומציג בכנות היכן מנוע האימות נעצר

הלחץ מאחורי תכונות אלו הוא רגולטורי, לא טכנולוגי. גרמניה וצרפת משיקות בהדרגה חובת חשבוניות דיגיטליות מובנות לעסקאות B2B מקומיות, ושני המנדטים מתכנסים לאותו מודל היברידי: חשבונית ה-PDF שה-ERP שלכם כבר מפיק חייבת לשאת קובץ XML תואם לקריאת מכונה בתוכה. לאפליקציית הנהלת חשבונות או ERP ב-Delphi המפיקה כיום חשבוניות PDF רגילות יש תאריך יעד קשיח להתחיל להפיק Factur-X או ZUGFeRD, וההבדל בין שני השמות קטן ממה שהשיווק מציע — Factur-X ו-ZUGFeRD הם אותו תקן צרפתי-גרמני המפורסם תחת שני מותגים שונים

מה הופך קובץ PDF לחשבונית Factur-X תקפה?

חשבונית Factur-X תקפה היא קובץ PDF/A-3 (ISO 19005-3) המטמיע בדיוק קובץ XML מובנה אחד של חשבונית כקובץ מקושר, מקשר אותו מקטלוג המסמך ומצהיר עליו במטא-נתונים של הרחבת XMP. שלוש שכבות חייבות להסכים ביניהן. ראשית, המכל חייב להתאים לתקן PDF/A-3, פרופיל הארכיון שאינו מגביל את סוג הקבצים המוטמעים (בשונה מ-PDF/A-1 ו-PDF/A-2). שנית, קובץ ה-XML של החשבונית, ששמו factur-x.xml עבור פרופילים סטנדרטיים או xrechnung.xml עבור הגרסה הגרמנית של XRechnung, חייב להירשם במערך /AF של הקטלוג ובעץ /Names /EmbeddedFiles, עם רשומת AFRelationship המציינת מהו הקובץ המצורף: הערך Alternative פירושו שה-XML הוא ייצוג שקול של החשבונית הגלויה, דרישה המשותפת לרוב הפרופילים. שלישית, סכימת הרחבת XMP של Factur-X חייבת לציין את שם הקובץ המוטמע, את רמת התאימות שלו ואת סוג המסמך, כדי שמערכת מקבלת תוכל לסווג את החשבונית מבלי לנתח את ה-XML כלל

קובץ ה-XML עצמו עוקב אחר המודל הסמנטי EN 16931, התקן האירופי המגדיר את ליבת החשבונית: קונה, מוכר, פריטי שורות, פירוט מס, סכום לתשלום, כל אחד עם מספר מונח עסקי (BT-1 למספר החשבונית, BT-5 למטבע וכדומה). לכן אימות EN 16931 מתרחש בשתי שכבות נפרדות. אימות סכימת XML בודק שהמסמך הוא CII מעוצב כהלכה עם שמות האלמנטים הנכונים. כללי העסק של Schematron בודקים שהתוכן קוהרנטי — שכלל BR-CO-15 מתקיים וסך החשבונית אכן שווה לסכום נטו של השורות בתוספת מע"מ, ושמונחי חובה קיימים עבור הפרופיל המוצהר. קובץ יכול לעבור אימות סכימה ועדיין להיות חשבונית לא תקפה, וזו הסיבה ששכבת כללי העסק קיימת

מדוע ZUGFeRD 2.5 אינו זקוק למכל חדש

גרסת ZUGFeRD 2.5 אינה משנה דבר ברמת מכל ה-PDF: מזהי ה-URN המנחים, סכימת ה-XMP ותגית הגרסה זהים לחלוטין לאלה של ZUGFeRD 2.3 / Factur-X 1.0. הדבר מפתיע את רוב המפתחים, ולכן כדאי לצטט את המקור. מפרט Factur-X 1.09 קובע כי מספר הגרסה ב-URI של סכימת הרחבת PDF/A אינו קשור למספר הגרסה של מפרט נתוני ה-XML, וכי הגרסה של מופעי חשבוניות Factur-X נשארת 1.0 — ב-XMP fx:Version וב-BT-24 של factur-x.xml כאחד. קובץ הדגימות הרשמי של ZUGFeRD 2.5 מאשר זאת: כל דגימה מטמיעה את factur-x.xml תחת URN מנחה של urn:factur-x.eu:1p0 עם fx:Version של 1.0. מה שגרסת 2.5 באמת מוסיפה חי בתוך מודל נתוני ה-XML — אלמנטים חדשים כמו שורות תת-חשבונית ומזהי אצוות — שהיא השכבה העסקית שהאפליקציה שלכם מייצרת, ולא שכבת המכל שספריית ה-PDF מנהלת

התוכנה HotPDF מיישמת ממצא זה ישירות. הפונקציה AddFacturXAssociatedFile מקבלת ארגומנט Version ומנרמלת אותו באמצעות HPDFFacturXNormalizeVersion לפני כתיבת ה-XMP, כך שקריאה המעבירה את הקיצור 2p5 עדיין מפיקה את התגית התואמת למפרט במקום להמציא גרסת XMP של 2p5 שאף מאמת לא יקבל. כלי הזיהוי מתייחסים ל-2p5 כאל שקול בעת הקריאה, כך שהסבב נשאר סימטרי. אם ספרייה או כלי כלשהו אומרים לכם שהם כותבים מכל מיוחד עבור ZUGFeRD 2.5, המפרט עצמו קובע שאין דבר כזה

הטמעת קובץ ה-XML של החשבונית מ-Delphi

הפונקציה THotPDF.AddFacturXAssociatedFile הופכת הרצת הפקה רגילה של PDF/A-3 לחשבונית Factur-X בקריאה אחת: היא מטמיעה את בייטי ה-XML כקובץ מקושר, רושמת את factur-x.xml במערך /AF של הקטלוג ובעץ /Names /EmbeddedFiles, ומפיקה את מטא-נתוני הרחבת XMP המתאימים ל-Factur-X. אתם מציירים את החשבונית הקריאה לבני אדם באמצעות ה-API הרגיל של HotPDF; עבודת המכל היא שורה אחת בין BeginDoc ל-EndDoc

var
  Pdf: THotPDF;
  InvoiceXML, UBLXML: TBytes;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.FileName := 'invoice-2026-0042.pdf';
    Pdf.PDFACompliance := '3B';            // PDF/A-3b container
    Pdf.BeginDoc;
    // embed the EN 16931 CII invoice XML as factur-x.xml
    InvoiceXML := BuildInvoiceXML;         // produced by your business layer
    Pdf.AddFacturXAssociatedFile(InvoiceXML, 'EN 16931');
    // optional: attach a UBL view as a supplementary representation
    UBLXML := BuildUBLXML;
    Pdf.AddUBLSupplementaryFile(UBLXML);   // factur-xubl.xml, Alternative
    // ... draw the visible invoice page here ...
    Pdf.EndDoc;
  finally
    Pdf.Free;
  end;
end;

ערכי ברירת המחדל עוקבים אחר המפרט: שם הקובץ factur-x.xml, יחס Alternative, גרסה 1.0. הפונקציה AddUBLSupplementaryFile מיישמת את סעיף 6.4 של מפרט Factur-X 1.09, המאפשר לייצוג UBL של אותה חשבונית להצטרף כקובץ מצורף שני בשם factur-xubl.xml עם סוג MIME של application/xml ויחס Alternative. הסדר כאן חשוב, ו-HotPDF אוכפת את המגבלה שהמפרט מרמז עליה: קובץ ה-UBL הוא ייצוג משלים, לעולם לא החשבונית הראשית, ולכן יש להוסיף אותו לאחר ש-AddFacturXAssociatedFile רשמה את ה-CII הראשי. שימו לב שהדף המצויר וה-XML המוטמע חייבים לציין את אותה חשבונית — קובץ PDF המציג סכום אחד ומקודד סכום אחר הוא בדיוק מצב הכשל שחשבונית היברידית נועדה למנוע. צד ה-PDF/A-3 של תהליך עבודה זה, כולל OutputIntent והטמעת גופנים, מכוסה בהרחבה במאמר הנלווה על אימות PDF/A,‏ PDF/X ו-PDF/UA ב-Delphi

כיצד מזהים חשבונית Factur-X ב-PDF נכנס?

הפונקציה THotPDF.DetectFacturXInvoice עונה על צד הקבלה של המנדט: טענו כל קובץ PDF וגלו בקריאה אחת האם הוא חשבונית היברידית, איזה פרופיל הוא מצהיר ואילו קבצים מצורפים הוא נושא. הפונקציה מחזירה רשומת THPDFFacturXInvoiceInfo עם שם קובץ ה-XML המוטמע, שם המסמך וסוגו המוצהרים ב-XMP, תגית הגרסה, רמת התאימות, URN המנחה וערך ה-AFRelationship. שני שדות נוספים מדווחים על ייצוגים משלימים: UBLFileName מתמלא כאשר קיים קובץ מצורף בשם factur-xubl.xml, ו-EDIFACTFileName כאשר קיים ייצוג EDIFACT — אף אחד מהם אינו משפיע על קביעת הזיהוי, מכיוון שייצוג משלים אינו יכול להתקיים ללא חשבונית ה-CII הראשית

var
  Reader: THotPDF;
  Info: THPDFFacturXInvoiceInfo;
begin
  Reader := THotPDF.Create(nil);
  try
    if Reader.LoadFromFile('incoming-invoice.pdf') <= 0 then
      raise Exception.Create('No pages loaded.');
    if Reader.DetectFacturXInvoice(Info) then
    begin
      Writeln('Invoice XML:    ', string(Info.FileName));
      Writeln('Conformance:    ', string(Info.ConformanceLevel));
      Writeln('Guideline URN:  ', string(Info.GuidelineID));
      Writeln('Relationship:   ', string(Info.AFRelationship));
      if Info.UBLFileName <> '' then
        Writeln('UBL view:       ', string(Info.UBLFileName));
    end
    else
      Writeln('Not a Factur-X / ZUGFeRD hybrid invoice.');
  finally
    Reader.Free;
  end;
end;

הזיהוי קורא את אותם סיגנלים שמאמת תואם קורא — סכימת הרחבת XMP ורישום הקבצים המקושרים — במקום לנחש לפי שמות קבצים בלבד. אותו אובייקט מסמך טעון חושף גם את מטא-נתוני ה-XMP ומילון ה-Info לעיון או לתיקון, תהליך עבודה המתואר במאמר על עריכת מטא-נתונים במסמכי PDF טעונים

כללי עסק או סכימה: מה פירוש אימות EN 16931?

הפונקציה HPDFValidateEInvoice, פונקציה עצמאית ביחידה HPDFEInvoiceValidator, מריצה את שתי שכבות האימות ברצף: תחילה בדיקת מבנה המכל באמצעות ValidateFacturXInvoice, ולאחר מכן את כללי העסק של Schematron EN 16931 מול קובץ ה-XML של החשבונית שחולץ. HotPDF מספקת את חמשת קובצי הכללים הרשמיים של Factur-X 1.09 — MINIMUM,‏ BASIC WL,‏ BASIC,‏ EN 16931 ו-EXTENDED — תחת הנתיב Lib/resources/Schematron/, והפונקציה HPDFSchematronFileForProfile בוחרת את הקובץ המתאים לרמת התאימות שזוהתה, כך שחשבונית EN 16931 נבדקת אוטומטית מול Factur-X_1.09_EN16931.sch. הפונקציה מחזירה True רק כאשר המכל תקין ואף כלל עסק לא הופר

uses HPDFEInvoiceValidator;

var
  Reader: THotPDF;
  Report: THPDFEInvoiceValidationReport;
  I: Integer;
begin
  Reader := THotPDF.Create(nil);
  try
    Reader.LoadFromFile('incoming-invoice.pdf');
    if HPDFValidateEInvoice(Reader, SchematronDir, Report) then
      Writeln('Container and business rules: OK')
    else
      Writeln('Validation found issues.');
    if Report.BusinessRulesEvaluated then
    begin
      Writeln(Report.BusinessSummary.Evaluated, ' rules evaluated, ',
        Report.BusinessSummary.Skipped, ' skipped (XPath 2.0)');
      for I := 0 to High(Report.BusinessRules) do
        if (not Report.BusinessRules[I].Skipped) and
           (Report.BusinessRules[I].Severity = stsError) then
          Writeln(string(Report.BusinessRules[I].RuleID), ': ',
            string(Report.BusinessRules[I].Message));
    end;
  finally
    Reader.Free;
  end;
end;

כל רשומה ב-BusinessRules נושאת את מזהה הכלל שנותח מטקסט האימות — BR-45,‏ BR-CO-17 ושאר ערכת השמות של EN 16931 — בתוספת XPath של הקשר הכלל, ביטוי הבדיקה וחומרה המשקפת את תכונת הדגל של Schematron. זה הופך את הדוח לשימושי: במקום תשובה פשוטה של עבר/נכשל אתם מקבלים את המונח העסקי המדויק שאינו עקבי, שזה מה שצוות התמיכה שלכם צריך כאשר לקוח שואל מדוע חשבונית נדחתה. צוותים שכבר מייצרים דוחות תאימות מובנים עבור קובצי PDF בארכיון יכולים לקפל תוצאות אלו לאותו צינור המתואר במאמר על אוטומציה של דוחות קדם-טיסה (preflight) ב-PDF

היכן המאמת נעצר, בכנות

מנוע ה-Schematron בנוי על MSXML, המעריך XPath 1.0. קובץ הכללים של EN 16931 מצהיר על queryBinding="xslt2" ונושא 424 טענות, שרובן משתמשות בפונקציות XPath 1.0 בלבד כמו string-length ו-substring-after — אלו רצות באופן טבעי. מיעוטן מסתמכות על מבני XPath 2.0 כמו המרות xs:decimal או exists(); התוכנה HotPDF סורקת מראש כל ביטוי בדיקה לאיתור תגיות XPath 2.0 מוכרות ומסמנת כללים אלה כ-Skipped עם חומרה של info, במקום לנסות הערכה שתכשיל את הריצה. רשומת הסיכום מדווחת על מספר הכללים שהוערכו ואלה שדלגו עליהם בנפרד, כך שהלוג שלכם מציג תמיד בדיוק כמה מתוך ערכת הכללים הופעל. בגרסת Delphi 7, שבה קישורי MSXML אינם זמינים, היחידה מקמפלת מעטפת המדווחת על המנוע כלא זמין מבלי לפגוע בקביעת המכל; המנוע המלא דורש את גרסת Delphi XE2 ומעלה

הגבול השני חשוב עוד יותר. תוצאה ירוקה מ-HPDFValidateEInvoice פירושה שמבנה המכל תואם ואף כלל עסק שהוערך לא הופר — אין זה אישור לתאימות מס. כללי הרחבה לאומיים, דרישות ספציפיות של מקבלים והתחייבויות משפטיות סביב תוכן החשבונית יושבים מעל ליבת EN 16931 ומחוץ לכל ספרייה. התייחסו למאמת כשער המונע מחשבוניות שבורות מבחינה מבנית לצאת מתיבת הדואר היוצא שלכם ומסמן חשבוניות לא עקביות בתיבת הדואר הנכנס שלכם, הריצו מאמת XPath 2.0 מלא בצינור השחרור שלכם כאשר תהליך ההסמכה דורש זאת, ותנו ליועצי המס שלכם את הבעלות על שאלת התאימות. ממשקי ה-API של ההטמעה, הזיהוי והאימות המוצגים כאן הם חלק מרכיב HotPDF הסטנדרטי עבור Delphi ו-C++Builder, עם דוגמת חשבונית דיגיטלית מלאה המכסה את כל שבעת הפרופילים של ZUGFeRD ו-Factur-X