XRechnung, Teil 2: Rechnungen einlesen

Nachdem wir im ersten Teil dieser Beitragsreihe gezeigt haben, wie Sie aus Daten wie Kundeninformationen, Rechnungsdatum und Rechnungspositionen ein XML-Dokument im XRechnung-Format erstellen, wollen wir in diesem Beitrag den umgekehrten Weg gehen: Wir wollen die Daten aus einer so generierten Rechnung auslesen und zurück in das Datenmodell schreiben. Dazu sind vor allem Fähigkeiten im Auslesen von XML-Dokumenten erforderlich – und der Umgang mit Namespace-Deklarationen in diesen Dokumenten. Nach der Lektüre dieses Beitrags sind Sie in der Lage, die Daten aus einer XRechnung automatisiert in ein entsprechendes Datenmodell einzulesen.

Ausgangssituation

Für bestimmte Empfänger müssen Rechnungen mittlerweile in einem automatisiert lesbaren Format vorliegen, zum Beispiel im Format XRechnung. Dieses Format hat gegenüber Rechnungen im PDF-Format den Vorteil, dass alle Informationen an der entsprechenden in der vorgegebenen XML-Struktur zu finden sind und diese somit maschinell verarbeitet werden können. Im Beitrag XRechnung, Teil 1: Rechnungen generieren (www.access-im-unternehmen.de/1277) haben wir die Daten aus einer Rechnungsverwaltung per Knopfdruck in ein solches Dokument geschrieben.

Nun ist es zu erwarten und auch wünschenswert, dass sich solche Formate allgemein durchsetzen, damit niemand mehr Rechnungen in gedruckter Form oder als PDF entgegennehmen und diese händisch verarbeiten muss. Deshalb werden früher oder später alle Unternehmen in der Lage sein müssen, solche Rechnungen zu verarbeiten.

Im vorliegenden Beitrag wollen wir die Daten aus einem solchen XRechnung-Dokument in ein vorgegebenes Datenmodell einlesen können. In einem weiteren Beitrag schauen wir uns dann an, wie wir die eingelesenen Daten in einem Bericht als herkömmliche Rechnung, lesbar für das menschliche Auge, präsentieren können.

Änderung im Datenmodell

Im Vergleich zum Datenmodell des ersten Teils der Beitragsreihe haben wir die Tabelle tblRechnungen leicht angepasst. Wir haben ein Feld namens Rechnungsnummer zum Speichern der jeweiligen Rechnungsnummer hinzugefügt und das Feld RechnungID mit dem Datentyp Autowert versehen. Zuvor mussten wir die Beziehung zwischen dem Feld RechnungID der Tabelle tblPositionen und der Tabelle tblRechnungen löschen und diese anschließend erneut anlegen.

Außerdem haben wir der Tabelle tblPositionen noch ein Feld namens Position hinzugefügt, mit dem die in der XRechnung angegebenen Positionsnummern gespeichert werden können, sowie ein Feld namens Einheit.

Beispielhafter Import

Der Standard der XRechnung umfasst natürlich alle denkbaren Informationen. Diese können wir im Rahmen dieses Beitrags nicht alle erfassen. Es geht hier allein darum, die grundlegenden Techniken für die Erfassung der gängigsten Informationen zu präsentieren.

Wenn Sie oder Ihre Kunden es mit Daten in XRechnungen zu tun bekommen, deren Übertragung hier nicht berücksichtigt wurde, so lernen Sie dennoch die Techniken, die nötig sind, die notwendigen Ergänzungen vorzunehmen.

Datenmodell für die Daten aus der

XRechnung

Das Datenmodell finden Sie in Bild 1. Jede Rechnung wird grundsätzlich in der Tabelle tblRechnungen erfasst und enthält dort einige grundlegende Daten wie das Rechnungsdatum, den Rechnungstyp, die Währung, eine Kundenreferenz sowie Verweise auf den Käufer und den Verkäufer. Käufer und Verkäufer werden in je einem Datensatz der Tabelle tblKontakte gespeichert und über Fremdschlüsselfelder der Tabelle tblRechnungen zugewiesen. Der Rechnungstyp wird aus einer Tabelle namens tblRechnungstypen ausgewählt.

Datenmodell für die Daten aus einer XRechnung

Bild 1: Datenmodell für die Daten aus einer XRechnung

Die einzelnen Rechnungspositionen landen schließlich in der Tabelle tblPositionen, die über das Fremdschlüsselfeld RechnungID dem jeweiligen Datensatz aus der Tabelle tblRechnungen zugewiesen werden.

Jede Position enthält Bezeichnung, Einzelpreis, Mehrwertsteuersatz und Menge, wobei der Mehrwertsteuersatz wiederum über ein Fremdschlüsselfeld aus der Tabelle tblMehrwertsteuersaetze ausgewählt wird.

Einlesen der Basisdaten

Der Anfang des XRechnung-Dokuments sieht wie in Listing 1 aus. Hier sehen wir zunächst das Root-Element ubl, das einige Namespaces aufweist, um die wir uns später explizit kümmern werden.

<ubl:Invoice xmlns:ubl="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2" 
     xmlns:cac="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2" 
     xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2" 
     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
     xsi:schemaLocation="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2 
     http://docs.oasis-open.org/ubl/os-UBL-2.1/xsd/maindoc/UBL-Invoice-2.1.xsd">
     <cbc:CustomizationID>urn:cen.eu:en16931:2017#compliant#urn:xoev-de:kosit:standard:xrechnung_1.2</cbc:CustomizationID>
     <cbc:ID>1234</cbc:ID>
     <cbc:IssueDate>2020-09-24</cbc:IssueDate>
     <cbc:InvoiceTypeCode>380</cbc:InvoiceTypeCode>
     <cbc:DocumentCurrencyCode>EUR</cbc:DocumentCurrencyCode>
     <cbc:BuyerReference>1234</cbc:BuyerReference>
     <cac:OrderReference>
         <cbc:ID/>
     </cac:OrderReference>
     <cac:ContractDocumentReference>
         <cbc:ID/>
     </cac:ContractDocumentReference>
     <cac:ProjectReference>
         <cbc:ID/>
     </cac:ProjectReference>
     ...
</ubl>

Listing 1: Basisdaten der Rechnung im XML-Dokument

Als erste untergeordnete Elemente folgen nun bereits die grundlegenden Rechnungsdaten. Das Element cbc:CustomizationID liefert das Format, in dem die Rechnung verfasst wurde, hier in der Version 1.2.

Das Element cbc:ID liefert die Rechnungsnummer, cbc:IssueDate das Rechnungsdatum. Mit dem Wert 380 im Feld cbc:InvoiceTypeCode erhalten wir einen Hinweis auf die Art der Rechnung. 380 steht für Commercial Invoice.

Das Element cbc:DocumentCurrencyCode liefert das Kürzel für die Währung der Rechnung. In der Regel finden wir hier die Einstellung EUR für Euro vor. Das Feld cbc:BuyerReference nimmt die sogenannte Leitweg-ID auf.

Außerdem gibt es noch einige weitere Elemente für verschiedene Referenzen wie OrderReference, ContractDocumentReference, ProjectReference et cetera, die wir an dieser Stelle jedoch nicht verarbeiten wollen.

Wir wollen den Einlesevorgang zunächst VBA-gesteuert ausführen, daher erstellen wir zuerst eine Prozedur, mit der wir die einzulesende Datei per Dateiauswahl-Dialog ermitteln und dann die eigentliche Prozedur zum Einlesen aufrufen. Die Prozedur zum Initialisieren des Einlesevorgangs sieht wie folgt aus:

Public Sub Test_XRechnungEinlesen()
     Dim strPfad As String
     strPfad = OpenFileName(CurrentProject.Path, "XRechnung auswählen", "XRechnung (*.xml)")
     XRechnungEinlesen strPfad
End Sub

Die Funktion OpenFileName finden Sie im Modul mdlFileDialog.

Die Funktion XRechnungEinlesen

Danach starten wir mit der Funktion XRechnungEinlesen den eigentlichen Einlesevorgang. Dazu prüfen wir in einer If…Then-Bedingung zunächst, ob ein Dateipfad mit strPfad übergeben wurde und ob die Datei überhaupt vorhanden ist (siehe Listing 2).

Public Function XRechnungEinlesen(strPfad As String) As Long
     Dim objXML As MSXML2.DOMDocument60
     Dim objInvoice As MSXML2.IXMLDOMNode
     Dim db As DAO.Database
     Set db = CurrentDb
     If Not Len(Dir(strPfad)) = 0 Then
         Set objXML = New MSXML2.DOMDocument60
         objXML.SetProperty "SelectionNamespaces", "xmlns:ubl=''urn:oasis:names:specification:ubl:schema:xsd:Invoice-2''" _
             & "xmlns:cac=''urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2'' " _
             & "xmlns:cbc=''urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2'' " _
             & "xmlns:xsi=''http://www.w3.org/2001/XMLSchema-instance''"
         objXML.Load strPfad
         Set objInvoice = objXML.childNodes.Item(0)
         XRechnungEinlesen = RechnungsdatenEinlesen(objInvoice, db)
     End If
End Function

Listing 2: Startprozedur zum Einlesen der XRechnung

Ist das der Fall, erstellt die Funktion ein neues Objekt des Typs MSXML2.DOMDocument60. Um diese verwenden zu können, benötigen wir einen Verweis auf die Bibliothek Microsoft XML, v6.0, die wir im Verweise-Fenster, das wir mit dem Menübefehl Extras|Verweise des VBA-Editors öffnen, markieren müssen (siehe Bild 2).

Hinzufügen eines Verweises auf die XML-Bibliothek

Bild 2: Hinzufügen eines Verweises auf die XML-Bibliothek

Nun kommt ein entscheidender Schritt, ohne den wir dieses mit einigen Namespace-Präfixen gespickte XML-Dokument nicht einlesen können: Wir stellen die Eigenschaft SelectionNamespaces mit der Methode SetProperty auf einen Wert ein, der die Definition der im Element ubl:Invoice des XML-Dokuments angegebenen Namespaces enthält.

Danach können wir die Load-Methode des Objekts objXML nutzen, um die unter strPfad angegebene XRechnung zu laden. Dann referenzieren wir das Root-Element, das wir mit objXML.childNodes.Item(0) ermitteln, mit der Variablen objInvoice und übergeben diese an die erste Unterfunktion RechnungsdatenEinlesen. Dieser übergeben wir auch den zuvor mit CurrentDb ermittelten Verweis auf das Database-Objekt der aktuellen Datenbankdatei.

Die Funktion RechnungsdatenEinlesen soll nach erfolgreichem Einlesen in die Tabelle tblRechnungen den Primärschüsselwert des neu erstellten Datensatzes zurückliefern, den wir wiederum als Rückgabewert der aufrufenden Funktion XRechnungEinlesen festlegen.

Funktion zum Einlesen der Rechnungsdaten

Die Funktion RechnungsdatenEinlesen erwartet das Root-Element sowie einen Verweis auf das aktuelle Database-Objekt als Parameter (siehe Listing 3).

Private Sub RechnungsdatenEinlesen(objInvoice As MSXML2.IXMLDOMElement, db As DAO.Database) As Long
     Dim rst As DAO.Recordset
     Dim strID As String
     Dim datIssueDate As Date
     Dim lngInvoiceTypeCode As Long
     Dim strDocumentCurrencyCode As String
     Dim strBuyerReference As String
     Dim objKaeufer As MSXML2.IXMLDOMNode
     Dim objVerkaeufer As MSXML2.IXMLDOMNode
     Dim objPaymentMeans As MSXML2.IXMLDOMNode
     Dim objInvoiceLines As MSXML2.IXMLDOMNodeList
     Dim lngVerkaeuferID As Long
     Dim lngKaeuferID As Long
     Dim lngRechnungID As Long
     strID = TextEinlesen(objInvoice, "cbc:ID")
     datIssueDate = TextEinlesen(objInvoice, "cbc:IssueDate")
     lngInvoiceTypeCode = TextEinlesen(objInvoice, "cbc:InvoiceTypeCode")
     strDocumentCurrencyCode = TextEinlesen(objInvoice, "cbc:DocumentCurrencyCode")
     strBuyerReference = TextEinlesen(objInvoice, "cbc:BuyerReference")
     Set objKaeufer = ElementEinlesen(objInvoice, "//cac:AccountingCustomerParty/cac:Party")
     lngKaeuferID = AdresseEinlesen(objKaeufer, db)
     Set objVerkaeufer = ElementEinlesen(objInvoice, "cac:AccountingSupplierParty/cac:Party")
     lngVerkaeuferID = AdresseEinlesen(objVerkaeufer, db)
     Set objPaymentMeans = ElementEinlesen(objInvoice, "cac:PaymentMeans")
     ZahlungsinformationenSchreiben objPaymentMeans, db, lngVerkaeuferID
     Set rst = db.OpenRecordset("SELECT * FROM tblRechnungen WHERE 1=2", dbOpenDynaset)
     With rst
         .AddNew
         !Rechnungsnummer = strID
         !Rechnungsdatum = datIssueDate
         !RechnungstypID = lngInvoiceTypeCode
         !Waehrung = strDocumentCurrencyCode
         !Kundenreferenz = strBuyerReference
         !VerkaeuferID = lngVerkaeuferID
         !KaeuferID = lngKaeuferID
         lngRechnungID = !RechnungID
         .Update
     End With
     Set objInvoiceLines = ElementeEinlesen(objInvoice, "cac:InvoiceLine")
     PositionenEinlesen objInvoiceLines, db, lngRechnungID
     RechnungsdatenEinlesen = lngRechnungID
End Sub

Listing 3: Hauptprozedur zum Einlesen der Rechnungsdaten

Sie deklariert einige Variablen des Typs IXMLDOMNode, um Unterelemente von objInvoice aufzunehmen, sowie ein Element des Typs IXMLDOMNodeList für die Rechnungspositionen in der XRechnung. Außerdem benötigen wir eine Recordset-Variable namens rst, der wir die Tabelle tblRechnungen zuweisen und über die wir den neuen Rechnungsdatensatz anlegen. Daneben dienen einige weitere Variablen wie strID, datIssueDate et cetera dazu, die Werte aus der XRechnung nach dem Auslesen und vor dem Eintragen in den neuen Datensatz temporär aufzunehmen.

Basisdaten der Rechnung aus der XRechnung einlesen

Die ersten Anweisungen lesen die Werte der entsprechenden Elemente aus der XRechnung ein. Dazu nutzen diese verschiedene Hilfsfunktionen, die wir am Ende des Beitrags beschreiben. Die erste heißt TextEinlesen und sucht aus einem IXMLDomNode-Element den Text des mit dem zweiten Parameter übergebenen Wertes aus – in der ersten Anweisung beispielsweise das Element cbc:ID. Dieser Wert landet in der Variablen strID. Das Gleiche geschieht mit den Elementen cbc:IssueDate, cbc:InvoiceTypeCode, cbc:DocumentCurrencyCode und cbc:BuyerReference. Damit erhalten wir die Basisdaten der Rechnung.

Danach nutzen wir eine weitere Hilfsfunktion namens ElementEinlesen, um das Unterelement mit dem Namen cac:AccountingCustomerParty/cac:Party mit der Variablen objKaeufer zu referenzieren. Dieser Teilbereich der XRechnung sieht beispielsweise wie in Listing 4 aus.

     <cac:AccountingCustomerParty>
         <cac:Party>
             <cac:PostalAddress>
                 <cbc:StreetName>Teststr. 1</cbc:StreetName>
                 <cbc:AdditionalStreetName/>
                 <cbc:CityName>Berlin</cbc:CityName>
                 <cbc:PostalZone>12121</cbc:PostalZone>
                 <cac:Country>
                     <cbc:IdentificationCode>DE</cbc:IdentificationCode>
                 </cac:Country>
             </cac:PostalAddress>
             <cac:PartyTaxScheme>
                 <cbc:CompanyID>DE232323232</cbc:CompanyID>
                 <cac:TaxScheme>
                     <cbc:ID>VAT</cbc:ID>
                 </cac:TaxScheme>
             </cac:PartyTaxScheme>
             <cac:PartyLegalEntity>
                 <cbc:RegistrationName>Müller AG</cbc:RegistrationName>
             </cac:PartyLegalEntity>
             <cac:Contact>
                 <cbc:Name>Klaus Müller</cbc:Name>
                 <cbc:Telephone>0123-2121212</cbc:Telephone>
                 <cbc:ElectronicMail>klaus@mueller.de</cbc:ElectronicMail>
             </cac:Contact>
         </cac:Party>
     </cac:AccountingCustomerParty>

Listing 4: Element mit den Daten des Rechnungsempfängers

Nachdem wir dieses Element aufgerufen haben, rufen wir die Funktion AdresseEinlesen auf und übergeben dieser das IXMLDOMNode-Element aus objKaeufer sowie einen Verweis auf das Database-Objekt. Der Übersicht halber beschreiben wir diese Funktion erst später. An dieser Stelle interessiert uns nur das Ergebnis dieser Funktion. Diese schreibt nämlich die Käuferdaten in die Tabelle tblKontakte und liefert den Wert des Primärschlüssels des neu hinzugefügten Datensatzes zurück. Diesen können wir dann in der Variablen lngKaeuferID zwischenspeichern und beim Erstellen eines neuen Rechnungsdatensatzes direkt in die Tabelle tblRechnungen schreiben.

Auf die gleiche Weise lesen wir auch noch die Daten des Verkäufers ein. Diese finden wir im Element cac:AccountingSupplierParty/cac:Party. Auch für dieses Element rufen wir die Funktion AdresseEinlesen auf, was dazu führt, dass auch der Verkäufer als neuer Datensatz in der Tabelle tblKontakte landet. Den Primärschlüsselwert des neuen Datensatzes speichern wir in der Variablen lngVerkaeuferID, um ihn später in der Tabelle tblRechnungen zu speichern.

Zahlungsinformationen erfassen

Mit der Variablen objPaymentMeans des Typs IXMLDOMNode referenzieren wir nun den folgenden Bereich der XRechnung, der die Zahlungsinformationen enthält:

<cac:PaymentMeans>
     <cbc:PaymentMeansCode>31</cbc:PaymentMeansCode>
     <cbc:PaymentID>Debit transfer</cbc:PaymentID>
     <cac:PayeeFinancialAccount>
         <cbc:ID>DE12121212121212121212</cbc:ID>
         <cbc:Name>Andre Minhorst</cbc:Name>
         <cac:FinancialInstitutionBranch>
             <cbc:ID/>
         </cac:FinancialInstitutionBranch>
     </cac:PayeeFinancialAccount>
</cac:PaymentMeans>

Mit den hier enthaltenen Informationen und der ID des Verkäufers rufen wir dann die Prozedur ZahlungsinformationenSchreiben auf. Diese fügt die Zahlungsinformationen wie die IBAN und den Kontoinhaber zum Datensatz des Verkäufers in der Tabelle tblKontakte hinzu.

Damit haben wir alle notwendigen Informationen eingelesen und können nun zum Schreiben der Daten in die Tabelle tblRechnungen schreiten. Das erledigen wir, indem wir ein neues Recordset auf Basis der Tabelle tblRechnungen erstellen, dass aber keine Datensätze enthalten soll – deshalb das Kriterium 1=2.

Hier fügen wir mit der AddNew-Methode einen neuen Datensatz hinzu und füllen die Felder der Tabelle mit den zuvor ermittelten Werten – unter anderem auch die Fremdschlüsselfelder VerkaeuferID und KaeuferID mit den Primärschlüsselwerten der zuvor in der Tabelle tblKontakte angelegten Datensätzen.

Ende des frei verfügbaren Teil. Wenn Du mehr lesen möchtest, hole Dir ...

den kompletten Artikel im PDF-Format mit Beispieldatenbank

diesen und alle anderen Artikel mit dem Jahresabo

Schreibe einen Kommentar