Τεχνικό Άρθρο

Υβριδικά Τιμολόγια Factur-X και ZUGFeRD στη Delphi

Ένα συμμορφούμενο (compliant) ηλεκτρονικό τιμολόγιο δεν είναι ένα PDF με ένα αρχείο XML συρραμμένο (stapled) στο πλάι. Είναι ένα ενιαίο (single) έγγραφο PDF/A-3 που φέρει το τιμολόγιο δύο φορές: μία φορά ως μια σελίδα που διαβάζει ένας άνθρωπος, και μία φορά ως ένα αναγνώσιμο από μηχανή (machine-readable) Cross Industry Invoice XML που είναι αποθηκευμένο μέσα στο αρχείο ως ένα συσχετισμένο αρχείο (associated file). Οι δύο αναπαραστάσεις (representations) περιγράφουν το ίδιο τιμολόγιο. Αυτή η διπλή φύση (dual nature) είναι όλη η ουσία (whole point) των οικογενειών μορφότυπων που απαιτούν πλέον οι ευρωπαϊκές εντολές (mandates), το Factur-X σε Γαλλία και Γερμανία, το ZUGFeRD στις γερμανόφωνες αγορές, και το XRechnung για την τιμολόγιση στον δημόσιο τομέα της Γερμανίας. Αυτό το άρθρο περιγράφει βήμα-βήμα (walks through) πώς το PDFlibPas συναρμολογεί (assembles) ένα τέτοιο υβριδικό τιμολόγιο (hybrid invoice) στη Delphi, πού τα πρότυπα (standards) αφήνουν περιθώριο για λάθη (get it wrong), και γιατί ένα προφίλ (profile) στον κατάλογο χρειάζεται έναν εντελώς ξεχωριστό δημιουργό (builder) XML

Τι είναι στην πραγματικότητα (actually) ένα υβριδικό τιμολόγιο

Η ορατή σελίδα και το ενσωματωμένο XML εξυπηρετούν (serve) διαφορετικούς αναγνώστες. Ένας υπάλληλος (clerk) που εγκρίνει μια πληρωμή κοιτάζει την αποδοσμένη (rendered) σελίδα. Ένα σύστημα πληρωτέων λογαριασμών (accounts-payable) προσλαμβάνει (ingests) το XML, διαβάζει τα σύνολα και την ανάλυση φόρων (tax breakdown) ως δομημένα (structured) πεδία, και καταχωρεί (books) την εγγραφή χωρίς κάποιος άνθρωπος να πληκτρολογήσει (keying) τίποτα. Το σημασιολογικό (semantic) περιεχόμενο αυτού του XML διέπεται (is governed) από το EN 16931, το ευρωπαϊκό πρότυπο (standard) που ορίζει το μοντέλο δεδομένων (data model) του τιμολογίου: ποια πεδία υπάρχουν, τι σημαίνουν, και ποια είναι υποχρεωτικά (mandatory). Το EN 16931 είναι ένα σημασιολογικό μοντέλο, όχι ένας μορφότυπος (file format) αρχείου. Το Factur-X, το ZUGFeRD 2.x, και το XRechnung πραγματοποιούν (realize) όλα αυτό το μοντέλο ως έγγραφο Cross Industry Invoice του UN/CEFACT, τη σύνταξη (syntax) που φέρει τα πεδία του EN 16931 στο καλώδιο (on the wire)

Για να είναι το έγγραφο τόσο αρχειοθετήσιμο (archivable) όσο και αυτοπεριγραφόμενο (self-describing), το κοντέινερ είναι το PDF/A-3, που ορίζεται από το ISO 19005-3. Το PDF/A-3 είναι το επίπεδο συμμόρφωσης (conformance level) που επιτρέπει αυθαίρετα (arbitrary) ενσωματωμένα αρχεία, το οποίο είναι ακριβώς αυτό που πρέπει να είναι ένα τιμολόγιο XML. Το PDF/A-2 απαγορεύει (forbids) την ενσωμάτωση αρχείων που δεν είναι τα ίδια PDF/A, επομένως ένα τιμολόγιο Factur-X δεν μπορεί να είναι PDF/A-2. Η επιλογή του PDF/A-3 δεν είναι επομένως προτίμηση, είναι μια απαίτηση (requirement) που ακολουθεί (follows) άμεσα από την επιθυμία ενσωμάτωσης δεδομένων μη-PDF σε ένα αρχειακό (archival) έγγραφο

Γιατί η σχέση (relationship) είναι Alternative (Εναλλακτική)

Η ενσωμάτωση των bytes είναι το εύκολο μέρος. Το ISO 32000 §7.11.4 ορίζει τη ροή ενσωματωμένου αρχείου (embedded file stream), το αντικείμενο που συγκρατεί το ακατέργαστο XML και τις παραμέτρους του. Το μέρος που καθιστά το αρχείο ένα έγκυρο συσχετισμένο αρχείο (associated file) είναι η §14.13, η οποία προσθέτει την έννοια του συσχετισμένου αρχείου και το κλειδί /AFRelationship. Αυτό το κλειδί δηλώνει (states) πώς τα ενσωματωμένα δεδομένα σχετίζονται με το περιεχόμενο στο οποίο επισυνάπτονται, και η τιμή που επιβάλλει το Factur-X είναι Alternative

Η επιλογή έχει σημασία επειδή οι άλλες τιμές θα ισχυρίζονταν (would assert) κάτι ψευδές (false) για το έγγραφο. Το Source (Πηγή) θα σήμαινε ότι το XML είναι το υλικό από το οποίο δημιουργήθηκε το ορατό περιεχόμενο, ένα κύριο (master) από το οποίο προέρχεται η σελίδα. Το Supplement (Συμπλήρωμα) θα σήμαινε ότι το XML προσθέτει πληροφορίες πέρα από (beyond) αυτές που δείχνει η σελίδα, ένα επιπλέον (extra) που δεν περιέχεται στην απόδοση. Κανένα από τα δύο δεν είναι αυτό που είναι ένα τιμολόγιο Factur-X. Το XML και η σελίδα είναι δύο ισοδύναμες (equivalent) εκφράσεις ενός τιμολογίου, φέροντας το ίδιο νομικό (legal) περιεχόμενο σε δύο μορφές. Το Alternative (Εναλλακτική) είναι η τιμή που λέει ακριβώς αυτό: μια ισοδύναμη εναλλακτική αναπαράσταση (representation) του ορατού περιεχομένου. Ένας επικυρωτής (validator) που διαβάζει οποιαδήποτε άλλη σχέση σε ένα αρχείο Factur-X θα το απορρίψει (reject), και δικαίως (rightly), επειδή η σχέση είναι ένας αναγνώσιμος από μηχανή (machine-readable) ισχυρισμός (claim) σχετικά με το σε τι χρησιμεύει το συνημμένο

Ο κατάλογος των προφίλ (The profile catalog)

Το δείγμα (sample) E-Invoice που αποστέλλεται (ships) με το PDFlibPas οδηγεί (drives) την ίδια διαδρομή δημιουργίας (generation path) σε έξι προφίλ, ορισμένα ως πίνακας (array) εγγραφών (records) στο InvoiceModel.pas. Κάθε προφίλ φέρει (carries) τις τιμές που χρειάζεται ο συγγραφέας (writer): ένα όνομα εμφάνισης (display name), το όνομα του ενσωματωμένου αρχείου (embedded file name), ένα επίπεδο συμμόρφωσης (conformance level), την /AFRelationship, μια έκδοση (version), έναν προαιρετικό κωδικό χώρας, και το GuidelineID URN που το XML ανακοινώνει (announces) μέσα στο περιβάλλον (context) του εγγράφου του

Τα έξι είναι τα Factur-X EN16931, Factur-X BASIC, Factur-X EXTENDED για τη Γαλλία, XRechnung 3.0, ZUGFeRD 1.0 COMFORT, και ZUGFeRD 2.0 BASIC. Το GuidelineID είναι το πεδίο που λέει σε έναν παραλήπτη (receiver) ακριβώς ποιο προφίλ να περιμένει, και οι τιμές είναι συγκεκριμένες. Το 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. Το όνομα του ενσωματωμένου αρχείου είναι μέρος της σύμβασης (contract) επίσης. Τα προφίλ Factur-X ενσωματώνουν το factur-x.xml, η XRechnung ενσωματώνει το xrechnung.xml, και τα προφίλ ZUGFeRD ενσωματώνουν τα ZUGFeRD-invoice.xml ή zugferd-invoice.xml. Ένας παραλήπτης σαρώνει (scans) τα ονόματα των συνημμένων (attachment names) για να βρει το τιμολόγιο, επομένως το όνομα του αρχείου δεν είναι κοσμητικό (cosmetic)

Μια λεπτομέρεια στον κατάλογο αξίζει να διαβαστεί προσεκτικά (carefully). Τα περισσότερα προφίλ χρησιμοποιούν τη σχέση Alternative, αλλά η καταχώριση (entry) XRechnung 3.0 στο δείγμα χρησιμοποιεί τη Source. Οι δύο μορφότυποι (formats) λογοδοτούν (answer) σε διαφορετικούς επικυρωτές και συμβάσεις (conventions), και το δείγμα ορίζει τη σχέση κάθε προφίλ από τον κατάλογο (catalog) αντί να κωδικοποιεί σκληρά (hard-coding) μια ενιαία (single) τιμή, γι' αυτό (which is why) και υπάρχει το πεδίο (field) ανά-προφίλ αντί για μια σταθερά (constant)

Η παγίδα (trap) του ZUGFeRD 1.0

Είναι δελεαστικό (tempting) να υποθέσουμε ότι κάθε προφίλ είναι το Cross Industry Invoice του EN 16931 με μικρές (minor) παραλλαγές (variations) στο πόσα προαιρετικά (optional) πεδία συμπληρώνετε (populate). Αυτό ισχύει (holds) για τα πέντε από τα έξι. Δεν ισχύει για το ZUGFeRD 1.0 COMFORT, και ο λόγος είναι δομικός (structural) παρά κοσμητικός

Τα σύγχρονα προφίλ εκπέμπουν (emit) ένα Cross Industry Invoice του UN/CEFACT με έκδοση χώρου ονομάτων (namespace) :100, του οποίου το ριζικό στοιχείο (root element) είναι το rsm:CrossIndustryInvoice. Το ZUGFeRD 1.0 προηγείται (predates) αυτού του σχήματος (schema). Είναι το CrossIndustryDocument του 2014 με έκδοση χώρου ονομάτων :1p0, και το ριζικό του στοιχείο είναι το rsm:CrossIndustryDocument. Τα URN των χώρων ονομάτων διαφέρουν, το ριζικό στοιχείο διαφέρει, και το δέντρο στοιχείων (element tree) διαφέρει καθ' όλη τη διάρκειά του (throughout): το σχήμα :1p0 ομαδοποιεί δεδομένα κάτω από τα ApplicableSupplyChainTradeAgreement, ApplicableSupplyChainTradeDelivery, και ApplicableSupplyChainTradeSettlement, ενώ το :100 χρησιμοποιεί τα ApplicableHeaderTradeAgreement, ApplicableHeaderTradeDelivery, και ApplicableHeaderTradeSettlement. Η ονομασία (naming) είναι αρκετά παρόμοια για να παραπλανήσει (mislead) και αρκετά διαφορετική για να σπάσει (break)

Η λέξη COMFORT στο όνομα του προφίλ περιγράφει πόσο πλούσια είναι τα δεδομένα, ένα προφίλ επιπέδου αυτοματοποίησης (automation-grade) με πλήρη στοιχεία γραμμής (line items), ανάλυση φόρων, και όρους πληρωμής, όχι ποιο σχήμα τα μεταφέρει. Επομένως δεν μπορείτε να πάρετε ένα έγγραφο :100 και να το μετονομάσετε (relabel) για το ZUGFeRD 1.0. Το δείγμα το χειρίζεται αυτό με μια σημαία (flag) σε κάθε εγγραφή προφίλ και δύο ξεχωριστές (separate) συναρτήσεις δημιουργού (builder functions), επιλέγοντας τη σωστή πριν καν δημιουργηθεί (generated) οποιοδήποτε 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;

Ο διαχωρισμός (split) δεν είναι μια λεπτομέρεια (nicety) υλοποίησης. Η τροφοδοσία (feeding) ενός δέντρου :100 σε έναν παραλήπτη (receiver) ZUGFeRD 1.0 παράγει ένα έγγραφο που αποτυγχάνει στην επικύρωση (validation) σχήματος στο ριζικό στοιχείο, επομένως οι δύο οικογένειες πρέπει να κατασκευαστούν (built) από κώδικα που γνωρίζει ποια γράφει

Επιλογή του επιπέδου PDF/A-3

Το PDF/A-3 έχει τρία επίπεδα συμμόρφωσης (conformance levels), και το PDFlibPas τα επιλέγει μέσω του SetPDFAMode. Η λειτουργία (mode) 5 είναι το PDF/A-3b, το επίπεδο που εγγυάται (guarantees) αξιόπιστη (reliable) οπτική αναπαραγωγή (visual reproduction). Η λειτουργία 6 είναι το PDF/A-3a, το οποίο προσθέτει την ετικετοποιημένη δομή (tagged-structure) και τις απαιτήσεις προσβασιμότητας (accessibility requirements) του επιπέδου a. Η λειτουργία 7 είναι το PDF/A-3u, το οποίο απαιτεί (requires) όλο το κείμενο να χαρτογραφείται (mapped) στο Unicode. Η ενεργοποίηση της λειτουργίας ενσωματώνει (embeds) επίσης την ενσωματωμένη (built-in) πρόθεση εξόδου (output intent) sRGB της βιβλιοθήκης, τον χαρακτηρισμό χρωμάτων που απαιτεί (demands) το PDF/A, ώστε το αποδοσμένο χρώμα να είναι καθορισμένο (defined) και όχι εξαρτώμενο από τη συσκευή (device-dependent)

Οι περισσότερες ροές (flows) τιμολογίων εκτελούνται (run) στο 3b, το οποίο είναι επαρκές (sufficient) για μια πιστή ορατή σελίδα συν (plus) το ενσωματωμένο XML. Εάν χρειάζεστε ένα ρητό (explicit) προφίλ ICC αντί για το ενσωματωμένο (built-in), η LoadOutputIntentProfile το ανταλλάσσει (swaps it in) αφού οριστεί η λειτουργία. Το δείγμα (sample) φορτώνει το προφίλ sRGB του αποθετηρίου (repository) με αυτόν τον τρόπο και επιστρέφει (falls back) στην ενσωματωμένη πρόθεση όταν το αρχείο δεν είναι προσβάσιμο (reachable), επομένως η πρόθεση εξόδου είναι πάντα παρούσα

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;

Κατασκευή του υβριδικού τιμολογίου

Με το κοντέινερ διαμορφωμένο (configured), τα υπόλοιπα είναι τρία βήματα με τη σειρά (in order): ορίστε τη λειτουργία PDF/A-3, σχεδιάστε τη σελίδα που διαβάζει ο άνθρωπος (human-readable), στη συνέχεια επισυνάψτε το XML ως ένα συσχετισμένο αρχείο (associated file). Η ορατή σελίδα είναι συνηθισμένο (ordinary) περιεχόμενο. Ο μόνος περιορισμός (constraint) που αξίζει να θυμάστε είναι ότι το PDF/A απαγορεύει τις μη-ενσωματωμένες (non-embedded) Standard 14 γραμματοσειρές, επομένως η σελίδα πρέπει να ενσωματώσει (embed) μια πραγματική μορφοποίηση (face) γραμματοσειράς παρά να αναφερθεί (reference) σε μια ενσωματωμένη (built-in)

Η επισύναψη είναι μια μοναδική (single) κλήση. Η AddFacturXAssociatedFileFromString παίρνει τα ακατέργαστα (raw) UTF-8 XML bytes συν (plus) τα μεταδεδομένα του προφίλ, γράφει τη ροή ενσωματωμένου αρχείου (embedded file stream), την καταχωρεί (registers) στον πίνακα /AF του καταλόγου (Catalog) που απαιτεί το PDF/A-3, εφαρμόζει την /AFRelationship, και δημιουργεί τα μεταδεδομένα XMP του ηλεκτρονικού τιμολογίου που αναγνωρίζουν (identifies) το έγγραφο ως Factur-X, ZUGFeRD, ή XRechnung. Ελέγχει επίσης ότι το αναγνωριστικό (ID) κατευθυντήριας γραμμής (guideline) του XML ταιριάζει με το επίπεδο συμμόρφωσης (conformance level) που ζητήσατε, οπότε μια αναντιστοιχία (mismatch) μεταξύ του XML που δημιουργήσατε και του προφίλ που ονομάσατε εντοπίζεται (is caught) παρά να αποστέλλεται σιωπηλά (silently shipped)

// 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);

Μια λεπτότητα (subtlety) στη διαδρομή (path) των δεδομένων είναι η κωδικοποίηση (encoding). Το ενσωματωμένο XML δηλώνει encoding="UTF-8", και η μέθοδος παίρνει τα bytes της ως AnsiString, οπότε ένα όνομα πωλητή ή αγοραστή που δεν είναι ASCII πρέπει να φτάσει στην κλήση ως ακατέργαστα (raw) οκτάδες (octets) UTF-8. Μια απλή μετατροπή (plain cast) μέσω της σελίδας κωδικοποίησης ANSI του συστήματος θα κατέστρεφε (would corrupt) αυτούς τους χαρακτήρες και θα παρήγαγε (quietly produce) αθόρυβα ένα τιμολόγιο του οποίου το XML δεν ταιριάζει πλέον (no longer matches) με τη δική του δήλωση. Το δείγμα κωδικοποιεί (encodes) σε UTF-8 ρητά (explicitly) πριν παραδώσει τα bytes (handing the bytes over), που είναι ο ασφαλής (safe) τρόπος για να τροφοδοτήσετε (feed) οποιοδήποτε byte-oriented PDF API από ένα string Unicode

Για την επισύναψη (attaching) XML που δεν είναι ένα αναγνωρισμένο (recognized) προφίλ ηλεκτρονικού τιμολογίου, η AddPDFA3AssociatedFileFromString είναι το γενικό αντίστοιχο (generic counterpart). Παίρνει ένα όνομα αρχείου, τύπο MIME, περιγραφή (description), σχέση (relationship), και bytes, και γράφει ένα απλό (plain) συσχετισμένο αρχείο (associated file) PDF/A-3 χωρίς κανένα μεταδεδομένο (metadata) ειδικό (specific) για τιμολόγια ή ελέγχους (checks) κατευθυντήριων γραμμών (guideline). Χρησιμοποιήστε το για συμπληρωματικά δεδομένα (supplementary data)· χρησιμοποιήστε τη μέθοδο Factur-X για τιμολόγια, ώστε τα μεταδεδομένα του προφίλ και η αντιστοιχία (match) κατευθυντήριας γραμμής να γραφτούν για εσάς

Μόλις παραχθεί το έγγραφο (document is produced), τα επόμενα ερωτήματα είναι αν περνάει (passes) την επικύρωση (validation) PDF/A και προσβασιμότητας (accessibility), και αν μπορεί να υπογραφεί (signed) χωρίς να σπάσει η συμμόρφωση (breaking compliance). Αυτά καλύπτονται στην αναλυτική παρουσίαση (walkthrough) preflight του PDF/A και του PDF/UA και στον πάγκο εργασίας (workbench) συμμόρφωσης και υπογραφής. Όλα αυτά αποστέλλονται (ships) ως μέρος της Βιβλιοθήκης PDF PDFlibPas της Delphi, μαζί με τα API του PDF/A, της ετικετοποίησης (tagging), και των ιδιοτήτων εγγράφου (document-property) πάνω στα οποία χτίζεται (builds on) η διαδρομή (path) του ηλεκτρονικού τιμολογίου