Positionswerte in VBA/Access auslesen: Steuer und Betrag

Thema
VB6
SDK-Mitglieder
FillZUGFeRD_Structure() GetItemTradeTax() Invoice_TradeItem InvoiceCurrencyCode Licensekey ReadTradeItemCSV() TradeItemsCount
Business Terms
BT-151 Code der Umsatzsteuerkategorie des in Rechnung BT-152 Umsatzsteuersatz für den in Rechnung gestellten Artikel BT-131 Nettobetrag der Rechnungsposition BT-5 Code für die Rechnungswährung BG-25 Rechnungsposition BG-30 Umsatzsteuerinformationen auf der Ebene der Rechnungsposition BT-126 Kennung der Rechnungsposition BT-129 In Rechnung gestellte Menge
Codebeispiele
VB6 / VBScript (COM), VB.NET
Veröffentlicht

Wer eine ZUGFeRD- oder XRechnung in Microsoft Access, Excel oder einer anderen VBA-Umgebung einliest, will für jede Rechnungsposition meist dieselben vier Werte: Umsatzsteuersatz, Steuerkategorie, Nettobetrag und Währung. Der naheliegende Weg über Invoice_TradeItem(i).TradeSettlement funktioniert in VB.NET, scheitert aber unter VBA. Dieser Artikel zeigt, warum das so ist und mit welchen Aufrufen der Aloaha ZUGFeRD SDK Sie die Werte über COM zuverlässig bekommen.

Das Problem: verschachtelte .NET-Strukturen über COM

Ein typischer erster Versuch in VBA sieht so aus:

' funktioniert unter VBA NICHT
Kategorie = alo.Invoice_TradeItem(i).TradeSettlement.TradeTax.CategoryCode
Steuersatz = alo.Invoice_TradeItem(i).TradeSettlement.TradeTax.RateApplicablePercent
Betrag = alo.Invoice_TradeItem(i).TradeSettlement.Monetary_Summation.LineTotalAmount.Amount
Waehrung = alo.Invoice_TradeItem(i).TradeSettlement.Monetary_Summation.LineTotalAmount.CurrencyID

Invoice_TradeItem liefert zwar ein Objekt zurück, aber TradeSettlement und die Ebenen darunter sind .NET-Structures (Werttypen), keine Klassen. Die COM-Schnittstelle, über die VBA mit der SDK spricht, kann solche verschachtelten Werttypen nicht als Objekte durchreichen. Je nach Office-Version endet der Zugriff mit einem Laufzeitfehler, einem leeren Wert oder „Objekt unterstützt diese Eigenschaft oder Methode nicht“. In VB.NET und C# tritt das Problem nicht auf, dort ist der Zugriff über die Struktur völlig in Ordnung.

Für COM-Clients bietet die SDK deshalb flache Aufrufe, die nur Zeichenketten und Zahlen übergeben.

Die passenden Aufrufe für VBA

WertEN-16931-FeldAufruf in VBA
Anzahl der PositionenBG-25 (Rechnungsposition)alo.TradeItemsCount
Umsatzsteuersatz der PositionBT-152alo.GetItemTradeTax i, Steuersatz, Kategorie
Steuerkategorie der Position (S, Z, E, AE, K, G, O, L, M)BT-151alo.GetItemTradeTax i, Steuersatz, Kategorie
Nettobetrag der PositionBT-131Spalte totalamount aus alo.ReadTradeItemCSV(vbTab)
WährungBT-5alo.InvoiceCurrencyCode

Eine eigene Währung pro Position gibt es in der EN 16931 nicht: Alle Positionsbeträge stehen immer in der Rechnungswährung BT-5. InvoiceCurrencyCode reicht deshalb für jede Zeile.

Rechnung laden

FillZUGFeRD_Structure liest wahlweise eine XML-Datei (ZUGFeRD, Factur-X, XRechnung) oder ein ZUGFeRD-PDF; aus dem PDF holt die SDK die eingebettete XML selbst heraus.

Dim alo As Object
Set alo = CreateObject("Aloaha_ZUGFeRD.XML")
alo.Licensekey = "IHR-LIZENZSCHLUESSEL"
If Not alo.FillZUGFeRD_Structure("C:\Rechnungen\eingang.pdf", "IHR-LIZENZSCHLUESSEL") Then
    MsgBox "Rechnung konnte nicht gelesen werden."
    Exit Sub
End If

Steuersatz und Steuerkategorie je Position

GetItemTradeTax füllt die übergebenen Variablen per Referenz (ByRef). Wichtig: Die Variablen müssen als String deklariert sein, sonst kann VBA sie nicht als Referenz übergeben. Der Index ist 0-basiert – die erste Position hat den Index 0, die letzte TradeItemsCount - 1.

Dim i As Long
Dim Steuersatz As String, Kategorie As String

For i = 0 To alo.TradeItemsCount - 1
    Steuersatz = "": Kategorie = ""
    alo.GetItemTradeTax i, Steuersatz, Kategorie
    Debug.Print "Position " & i + 1, Kategorie, Steuersatz
Next i

Optional liefert derselbe Aufruf auch Text und Code des Befreiungsgrunds (entsprechend BT-120 und BT-121 auf Rechnungsebene), falls die Position steuerbefreit ist:

Dim Grund As String, GrundCode As String
alo.GetItemTradeTax i, Steuersatz, Kategorie, Grund, GrundCode

Nettobetrag der Position über ReadTradeItemCSV

ReadTradeItemCSV gibt alle Positionen als Text zurück: in der ersten Zeile die Spaltennamen, danach eine Zeile pro Position, getrennt durch vbCrLf. Den Nettobetrag der Position (BT-131) finden Sie in der Spalte totalamount. Suchen Sie die Spalte über ihren Namen statt über eine feste Nummer – dann bleibt Ihr Code auch bei künftigen zusätzlichen Spalten richtig.

Dim Zeilen() As String, Kopf() As String, Felder() As String
Dim k As Long, colBetrag As Long
Dim Waehrung As String

Waehrung = alo.InvoiceCurrencyCode
Zeilen = Split(alo.ReadTradeItemCSV(vbTab), vbCrLf)
Kopf = Split(Zeilen(0), vbTab)

colBetrag = -1
For k = 0 To UBound(Kopf)
    If Kopf(k) = "totalamount" Then colBetrag = k
Next k

For i = 1 To UBound(Zeilen)
    If Len(Zeilen(i)) > 0 Then
        Felder = Split(Zeilen(i), vbTab)
        Debug.Print "Position " & i, Val(Felder(colBetrag)), Waehrung
    End If
Next i

Drei Hinweise dazu:

  • Tabulator als Trennzeichen. Standard ist das Komma. Weil Artikelbezeichnungen und Beschreibungen selbst Kommas enthalten können, ist vbTab sicherer.
  • Dezimalpunkt. Beträge und Prozentsätze stehen so in der Liste, wie sie in der XML stehen, also mit Punkt (119.00). Val() wertet den Punkt unabhängig von den Ländereinstellungen richtig aus; CDbl() oder CCur() würden auf einem deutschen System falsch rechnen.
  • Textspalten sind HTML-kodiert. Freitextfelder wie productname, description oder ItemNote enthalten Sonderzeichen in kodierter Form (& statt &). Für Beträge, Sätze und Codes spielt das keine Rolle.

Weitere Spalten der Positionsliste

Die Liste enthält deutlich mehr als nur den Betrag. Die wichtigsten Spalten:

SpalteInhaltFeld
LineIDPositionsnummerBT-126
AssignedIDArtikelnummer des VerkäufersBT-155
productnameArtikelbezeichnungBT-153
quantity, unitMenge und EinheitBT-129, BT-130
netpriceNettopreisBT-146
grosspriceBruttopreis vor PreisnachlassBT-148
totalamountNettobetrag der PositionBT-131
taxpercentUmsatzsteuersatzBT-152
taxcategorycodeSteuerkategorieBT-151
DeliveryNoteLieferscheinnummer der Position–

Steuersatz und Kategorie stehen also auch in der Liste. Wenn Sie ohnehin alle Positionsdaten in eine Access-Tabelle übernehmen, reicht ReadTradeItemCSV allein; GetItemTradeTax lohnt sich, wenn Sie gezielt nur die Steuerdaten einer Position brauchen oder den Befreiungsgrund benötigen.

Komplettbeispiel für Access

Das folgende Beispiel liest eine Rechnung ein und schreibt jede Position mit Betrag, Steuersatz, Kategorie und Währung in eine Tabelle tblPositionen.

Public Sub PositionenEinlesen(ByVal Pfad As String)
    Dim alo As Object
    Dim rs As DAO.Recordset
    Dim Zeilen() As String, Kopf() As String, Felder() As String
    Dim i As Long, k As Long
    Dim colPos As Long, colBetrag As Long, colSatz As Long, colKat As Long
    Dim Waehrung As String

    Set alo = CreateObject("Aloaha_ZUGFeRD.XML")
    alo.Licensekey = "IHR-LIZENZSCHLUESSEL"
    If Not alo.FillZUGFeRD_Structure(Pfad, "IHR-LIZENZSCHLUESSEL") Then Exit Sub
    If alo.TradeItemsCount = 0 Then Exit Sub

    Waehrung = alo.InvoiceCurrencyCode
    Zeilen = Split(alo.ReadTradeItemCSV(vbTab), vbCrLf)
    Kopf = Split(Zeilen(0), vbTab)
    For k = 0 To UBound(Kopf)
        Select Case Kopf(k)
            Case "LineID": colPos = k
            Case "totalamount": colBetrag = k
            Case "taxpercent": colSatz = k
            Case "taxcategorycode": colKat = k
        End Select
    Next k

    Set rs = CurrentDb.OpenRecordset("tblPositionen", dbOpenDynaset)
    For i = 1 To UBound(Zeilen)
        If Len(Zeilen(i)) > 0 Then
            Felder = Split(Zeilen(i), vbTab)
            rs.AddNew
            rs!PositionNr = Felder(colPos)
            rs!Nettobetrag = Val(Felder(colBetrag))
            rs!Steuersatz = Val(Felder(colSatz))
            rs!Steuerkategorie = Felder(colKat)
            rs!Waehrung = Waehrung
            rs.Update
        End If
    Next i
    rs.Close
End Sub

Ersetzen Sie IHR-LIZENZSCHLUESSEL durch Ihren Lizenzschlüssel. Für einen Test genügt eine Testlizenz.

Und in VB.NET oder C#?

Dort können Sie weiterhin direkt auf die Struktur zugreifen, zum Beispiel alo.Invoice_TradeItem(i).TradeSettlement.TradeTax.CategoryCode. Die hier gezeigten Aufrufe funktionieren aber ebenso, und wer Code zwischen VBA und .NET teilt, fährt mit ihnen einheitlich.

Häufige Fragen

Warum kann ich in VBA nicht auf Invoice_TradeItem(i).TradeSettlement zugreifen?

TradeSettlement ist eine .NET-Structure, also ein Werttyp. Über COM lassen sich solche verschachtelten Werttypen nicht als Objekte an VBA weitergeben. Nutzen Sie stattdessen GetItemTradeTax und ReadTradeItemCSV.

Ist der Positionsindex 0- oder 1-basiert?

0-basiert. Die Schleife läuft von 0 bis TradeItemsCount - 1. In der Liste von ReadTradeItemCSV ist Zeile 0 die Kopfzeile, die erste Position steht also in Zeile 1.

Woher bekomme ich die Währung einer einzelnen Position?

Aus der Rechnungswährung BT-5 über InvoiceCurrencyCode. Die EN 16931 kennt keine abweichende Währung pro Position; alle Positionsbeträge stehen in der Rechnungswährung.

Warum rechnet CDbl mit den Beträgen falsch?

Die Beträge stehen mit Dezimalpunkt in der Rechnung. CDbl und CCur richten sich nach den Ländereinstellungen und lesen den Punkt auf deutschen Systemen als Tausendertrennzeichen. Val() wertet den Punkt immer als Dezimaltrennzeichen.

Kann ich statt einer XML auch ein ZUGFeRD-PDF einlesen?

Ja. FillZUGFeRD_Structure erkennt die Endung .pdf und liest die eingebettete XML aus dem PDF selbst aus.

Business Terms in diesem Artikel: BT-151 BT-152 BT-131 BT-5 BG-25 BG-30 BT-126 BT-129 BT-146