Import tables directly into Access from SAP using RFCs

Jimbo's picture

Access is an invaluable tool for data migration, transformation and manipulation. By calling a series of queries through a macro it is possible to automate all of the transformation steps and reduce the risk of corruption caused by manual processing.

One way to get the contents of a table is to export it to a text file and then import it into Access. Part of this can be automated through macros and VBA, but it includes cumbersome manual steps and each is prone to error.

Pulling the data into Access using Remote Function Calls (RFCs) into the SAP system is very easy. It requires only a user account with appropriate permissions and the libraries that are installed along with Office and the SAPGui client.

RFC calls from Access can be included in a Macro in the event that the data must be the most recent available. A good example is this obsolescence determination tool that requires current data extracted from a legacy SAP system.

The RFC_READ_TABLE function accepts as parameters the name of the SAP table, the name of the table to be created (or replaced) in Access and a comma-delimited string containing the names of the fields to be extracted. In this example the fields MATNR, LVORM, MSTAE, MSTAV, ERSDA and MTART are pulled from the SAP MARA table into the Access tblMARA table.

Function ReadFromSAP() As Boolean
    Dim x
    x = RFC_READ_TABLE("MARA", "tblMARA", "MATNR,LVORM,MSTAE,MSTAV,ERSDA,MTART")
End Function

The system information and user credentials can be hard-coded into the function, but in the sample below they are passed in via the TempVars variables. This code uses a library that is installed as part of the SAPGui program and will not work without SAPGui installed.

Function RFC_READ_TABLE(strSAPTable As String, strTableName As String, strFields As String) As Boolean

    Dim strTemp As String
    
    'Create the database parallel to SAP's . . .
    Dim nCounter As Long, nCurrent As Long
    Dim dbs As dao.Database
    Set dbs = CurrentDb
    Dim tdf As dao.TableDef
    Dim fld1 As dao.Field
    Dim fld2 As dao.Field
    Dim fName As String
    Dim fType As Integer
    Dim fSize As Integer
    Dim nFieldData(999, 2) As Long
    Dim strRow As String
    Dim strFieldnames() As String
    Dim vField As Variant
    Dim j As Integer
    Dim rs As Recordset
    Dim SQL As String
    Dim nCurRec As Long, nCurSec As Long
    Dim isUpdate As Boolean
    Dim strFieldName As String
    Dim nRowCount As Long
    Dim nTotalRecords As Long

    Dim RetVal As Variant, nSecondsLeft As Long, nTotalSeconds As Long
    
    Dim R3, MyFunc, App As Object
    
    ' Define the objects to hold IMPORT parameters
    Dim QUERY_TABLE As Object
    Dim DELIMITER   As Object
    Dim NO_DATA     As Object
    Dim ROWSKIPS    As Object
    Dim ROWCOUNT    As Object
    
    ' Define the objects to hold the EXPORT parameters
    ' None for RFC_TABLE_READ

    ' Define the objects to hold the TABLES parameters
    ' Where clause
    Dim OPTIONS As Object
    ' Fill with fields to return.  After function call will hold
    ' detailed information about the columns of data (start position
    ' of each field, length, etc.
    Dim FIELDS  As Object
    ' Holds the data returned by the function
    Dim DATA    As Object
    
    ' Use to write out results
    Dim ROW As Object
    
    Dim Result As Boolean
    Dim iRow, iColumn, iStart, iStartRow, iField, iLength As Integer
    
    '**********************************************
    'Create Server object and Setup the connection
    '**********************************************
    
    Dim strSAP_System As String
    Dim strSAP_SystemNumber As String
    Dim strSAP_Client As String
    Dim strSAP_User As String
    Dim strSAP_Password As String
    Dim strSAP_Language As String
    Dim strSAP_ApplicationServer As String

    nTotalSeconds = 0

    Set R3 = CreateObject("SAP.Functions")
    R3.Connection.System = [TempVars]![SAP_System]
    R3.Connection.SystemNumber = [TempVars]![SAP_SystemNumber]
    R3.Connection.client = [TempVars]![SAP_Client]
    R3.Connection.User = [TempVars]![SAP_User]
    R3.Connection.Password = [TempVars]![SAP_Password]
    R3.Connection.language = "EN"
    R3.Connection.ApplicationServer = [TempVars]![SAP_ApplicationServer]
       
    RetVal = SysCmd(acSysCmdSetStatus, "Connecting to " & strSAP_System & " . . . ")

    If R3.Connection.logon(0, True) <> True Then
       If R3.Connection.logon(0, False) <> True Then
            Exit Function
       End If
    End If

    '*****************************************************
    'Call RFC function RFC_READ_TABLE
    '*****************************************************
    Set MyFunc = R3.Add("RFC_READ_TABLE")

    ' Set the Objects to the parameter they will return
    Set QUERY_TABLE = MyFunc.exports("QUERY_TABLE")
    Set DELIMITER = MyFunc.exports("DELIMITER")
    Set NO_DATA = MyFunc.exports("NO_DATA")
    Set ROWSKIPS = MyFunc.exports("ROWSKIPS")
    Set ROWCOUNT = MyFunc.exports("ROWCOUNT")

    Set OPTIONS = MyFunc.Tables("OPTIONS")
    Set FIELDS = MyFunc.Tables("FIELDS")

    QUERY_TABLE.Value = strSAPTable
    DELIMITER.Value = vbTab
    NO_DATA = ""
    
    j = 0
    strFieldnames = Split(strFields, ",")
    For Each vField In strFieldnames
        j = j + 1
        FIELDS.Rows.Add
        FIELDS.Value(j, "FIELDNAME") = vField
    Next
    
    RetVal = SysCmd(acSysCmdSetStatus, "Extracting " & j & " fields from table " & strSAPTable & " in " & strSAP_System & " . . . ")

    Result = MyFunc.CALL

    If Result = True Then
        Set DATA = MyFunc.Tables("DATA")
        Set FIELDS = MyFunc.Tables("FIELDS")
        Set OPTIONS = MyFunc.Tables("OPTIONS")
    Else
        MsgBox MyFunc.EXCEPTION
        R3.Connection.LOGOFF
        Exit Function
    End If
    
    '*******************************************
    'Quit the SAP Application
    '*******************************************
    R3.Connection.LOGOFF
    
    If Result <> True Then
      MsgBox (MyFunc.EXCEPTION)
      Exit Function
    End If


    nTotalColumns = FIELDS.ROWCOUNT
    'Check for existence of TargetTable
    nCounter = 0
    Do While nCounter < dbs.TableDefs.Count
        If dbs.TableDefs(nCounter).Name = strTableName Then
            'Delete TargetTable--must start from scratch
            dbs.TableDefs.Delete (strTableName)
        End If
        nCounter = nCounter + 1
    Loop
    
    
    Set tdf = dbs.CreateTableDef(strTableName)
    For iField = 1 To FIELDS.ROWCOUNT
        fName = FIELDS(iField, "FIELDNAME")
        fType = dbText
        fSize = FIELDS(iField, "LENGTH")
        Set fld1 = tdf.CreateField(fName, fType, fSize)
        fld1.AllowZeroLength = True
        fld1.Required = False
        tdf.FIELDS.Append fld1
        nFieldData(iField, 1) = FIELDS(iField, "OFFSET")
        nFieldData(iField, 2) = FIELDS(iField, "LENGTH")
    Next
    
    'Create the table in the database
    dbs.TableDefs.Append tdf
    dbs.TableDefs.Refresh

    'Open the table in the Database
    '**************************************
    Set rs = dbs.OpenRecordset(strTableName)
        
    'Display Contents of the table
    '**************************************

    'Class this function up a little...
    RetVal = SysCmd(acSysCmdClearStatus)
    RetVal = SysCmd(acSysCmdInitMeter, "Importing " & strTableName & " from SAP...", DATA.ROWCOUNT)

    iField = 1
    nRowCount = FIELDS.ROWCOUNT
    For iField = 1 To nRowCount
        nFieldData(iField, 1) = FIELDS(iField, "OFFSET")
        nFieldData(iField, 2) = FIELDS(iField, "LENGTH")
    Next
    For iRow = 1 To DATA.ROWCOUNT
        
        nTotalRecords = nTotalRecords + 1
        If Second(Now()) <> nCurSec Then ' And nCurRec <> rs.RecordCount Then
            nCurSec = Second(Now())
            nTotalSeconds = nTotalSeconds + 1
            nSecondsLeft = Int(((nTotalSeconds / iRow) * DATA.ROWCOUNT) * ((DATA.ROWCOUNT - iRow) / DATA.ROWCOUNT))
            RetVal = SysCmd(acSysCmdRemoveMeter)
            RetVal = SysCmd(acSysCmdInitMeter, "Importing " & strTableName & " from SAP, " & nSecondsLeft & " seconds remaining. [" & nTotalRecords & " of " & DATA.ROWCOUNT & "]", DATA.ROWCOUNT)
            RetVal = SysCmd(acSysCmdUpdateMeter, iRow)
            RetVal = DoEvents()
        End If

        
    '   Add a new row to the DB
        strRow = DATA(iRow, 1) 'Pull this into a string rather than pull it from DATA each time...
        
        rs.AddNew
        For iField = 1 To nRowCount
           rs.FIELDS(iField - 1).Value = Trim(Mid(strRow, nFieldData(iField, 1) + 1, nFieldData(iField, 2)))
        Next
        rs.Update
     Next
        
    rs.Close
    Close #2
    Set dbs = Nothing
    Set rs = Nothing
    RetVal = SysCmd(acSysCmdRemoveMeter)
End Function

Update:

When the table is so large that is causes overflow errors in the program called by the RFC, the table can be extracted in blocks. This example of the same code incorporates an offset and length to break the table up into manageable blocks that are appended to the table one after the other.

Mike Burnett gets special credit here for creating a need for this updated code and for the help with this programmer's rusty VBA skills. The original function was written in 2004 and the updated version is from 2019.

Option Compare Database


Sub QuickTest()
  Dim a As Boolean
  a = SAP_RFC_READ_TABLE("KNVP", "tblKNVP", "")
End Sub

Function SAP_RFC_READ_TABLE(strSAPTable As String, strTableName As String, strFields As String, Optional strOptions As String = "") As Boolean

'Função para ler as tabelas diretamente do SAP
'Adaptada de http://saplsmw.com/Import_tables_directly_into_Access_from_SAP_using_RFCs
'strSAPTable = Tabela SAP a ser lida Ex: "TACTZ"
'strTableName = Tabela Access (Será apagada e reescrita) Ex: "BASE_TACTZ"
'strFields = Nome dos campos que vão ser lidos, separados por virgula Ex: "BROBJ,ACTVT"
'strOption = Funciona como um where para a leitua. As aspas do exemplo são obrigatórias Ex. "ACTVT = '01'"

    Dim strTemp As String
    
    'Create the database parallel to SAP's . . .
    Dim nCounter As Long, nCurrent As Long
    Dim dbs As dao.Database
    Set dbs = CurrentDb
    Dim tdf As dao.TableDef
    Dim fld1 As dao.Field
    Dim fld2 As dao.Field
    Dim fName As String
    Dim fType As Integer
    Dim fSize As Integer
    Dim nFieldData(999, 2) As Long
    Dim strRow As String
    Dim strFieldnames() As String
    Dim strConvOptions() As String
    Dim FinalRFCQuery As String
    Dim vField As Variant
    Dim j As Integer
    Dim rs As Recordset
    Dim SQL As String
    Dim nCurRec As Long, nCurSec As Long
    Dim isUpdate As Boolean
    Dim strFieldName As String
    Dim nRowCount As Long
    Dim nTotalRecords As Long

    Dim RetVal As Variant, nSecondsLeft As Long, nTotalSeconds As Long
    Dim R3, MyFunc, App As Object

    ' Define the objects to hold IMPORT parameters
    Dim QUERY_TABLE As Object
    Dim DELIMITER   As Object
    Dim NO_DATA     As Object
    Dim ROWSKIPS    As Object
    Dim ROWCOUNT    As Object
    
    ' Define the objects to hold the EXPORT parameters
    ' None for RFC_TABLE_READ

    ' Define the objects to hold the TABLES parameters
    ' Where clause
    Dim OPTIONS As Object
    ' Fill with fields to return.  After function call will hold
    ' detailed information about the columns of data (start position
    ' of each field, length, etc.
    Dim FIELDS  As Object
    ' Holds the data returned by the function
    Dim DATA    As Object
    
    ' Use to write out results
    Dim ROW As Object
    
    Dim Result As Boolean
    Dim iRow, iColumn, iStart, iStartRow, iField, iLength As Integer
    
    '**********************************************
    'Create Server object and Setup the connection
    '**********************************************
    
    Dim strSAP_System As String
    Dim strSAP_SystemNumber As String
    Dim strSAP_Client As String
    Dim strSAP_User As String
    Dim strSAP_Password As String
    Dim strSAP_Language As String
    Dim strSAP_ApplicationServer As String
    
    
    '***********************************
    ' Set up an offset and length loop.
    '***********************************
    Const nMaxRowCount = 100000
    Dim nRowSkip
    nRowSkip = 0
    

    nTotalSeconds = 0

    Set R3 = CreateObject("SAP.Functions")
    R3.Connection.System = "P00"
    R3.Connection.SystemNumber = "00"
    R3.Connection.client = "300"
    R3.Connection.User = "your_userid"
    R3.Connection.Password = "your_password"
    R3.Connection.Language = "EN"
    R3.Connection.ApplicationServer = "severhostname.domain.com"

    RetVal = SysCmd(acSysCmdSetStatus, "Connecting to " & strSAP_System & " . . . ")

   If R3.Connection.logon(0, True) <> True Then
       'Couldn't log in silently, try with GUI...
       If R3.Connection.logon(0, False) <> True Then
            Exit Function
       End If
    End If

    '*****************************************************
    'Call RFC function RFC_READ_TABLE
    '*****************************************************
    Set MyFunc = R3.Add("RFC_READ_TABLE")

    ' Set the Objects to the parameter they will return
    Set QUERY_TABLE = MyFunc.exports("QUERY_TABLE")
    Set DELIMITER = MyFunc.exports("DELIMITER")
    Set NO_DATA = MyFunc.exports("NO_DATA")
    Set ROWSKIPS = MyFunc.exports("ROWSKIPS")
    Set ROWCOUNT = MyFunc.exports("ROWCOUNT")

    Set OPTIONS = MyFunc.tables("OPTIONS")
    Set FIELDS = MyFunc.tables("FIELDS")

    QUERY_TABLE.Value = strSAPTable
    DELIMITER.Value = vbTab
    NO_DATA = ""
    ROWSKIPS.Value = nRowSkip
    ROWCOUNT.Value = nMaxRowCount
    
    'Quebrando o nome dos campos
    j = 0
    strFieldnames = Split(strFields, ",")
    For Each vField In strFieldnames
        j = j + 1
        FIELDS.Rows.Add
        FIELDS.Value(j, "FIELDNAME") = Trim(UCase(vField))
    Next
    
    'Quebrando Lendo condições Where
    j = 0
    If strOptions <> "" Then
        strConvOptions = Split(strOptions, ",")
        FinalRFCQuery = ""
        For Each vField In strConvOptions
            FinalRFCQuery = FinalRFCQuery & Replace(vField, "=", " EQ ", Compare:=vbTextCompare) & " AND "
        Next
        'Terminando a clausula Where
        FinalRFCQuery = Left(FinalRFCQuery, Len(FinalRFCQuery) - 4)
        OPTIONS.Rows.Add
        OPTIONS.Value(1, 1) = Trim(UCase(FinalRFCQuery))
    End If

    RetVal = SysCmd(acSysCmdSetStatus, "Extracting " & j & " fields from table " & strSAPTable & " in " & strSAP_System & " . . . ")

    Result = MyFunc.CALL

    If Result = True Then
        Set DATA = MyFunc.tables("DATA")
        Set FIELDS = MyFunc.tables("FIELDS")
        Set OPTIONS = MyFunc.tables("OPTIONS")
    Else
        MsgBox MyFunc.EXCEPTION
        R3.Connection.logoff
        Exit Function
    End If

    nTotalColumns = FIELDS.ROWCOUNT
    'Check for existence of TargetTable
    nCounter = 0
    Do While nCounter < dbs.TableDefs.Count
        If dbs.TableDefs(nCounter).Name = strTableName Then
            'Delete TargetTable--must start from scratch
            dbs.TableDefs.Delete (strTableName)
        End If
        nCounter = nCounter + 1
    Loop
    
    
    Set tdf = dbs.CreateTableDef(strTableName)
    For iField = 1 To FIELDS.ROWCOUNT
        fName = FIELDS(iField, "FIELDNAME")
        fType = dbText
        fSize = FIELDS(iField, "LENGTH")
        Set fld1 = tdf.CreateField(fName, fType, fSize)
        fld1.AllowZeroLength = True
        fld1.Required = False
        tdf.FIELDS.Append fld1
        nFieldData(iField, 1) = FIELDS(iField, "OFFSET")
        nFieldData(iField, 2) = FIELDS(iField, "LENGTH")
    Next
    
    'Create the table in the database
    dbs.TableDefs.Append tdf
    dbs.TableDefs.Refresh

    'Open the table in the Database
    '**************************************
    Set rs = dbs.OpenRecordset(strTableName)
        
    'Display Contents of the table
    '**************************************

    'Class this function up a little...
    RetVal = SysCmd(acSysCmdClearStatus)
    RetVal = SysCmd(acSysCmdInitMeter, "Importing " & strTableName & " from SAP...", DATA.ROWCOUNT)

    iField = 1
    nRowCount = FIELDS.ROWCOUNT
    For iField = 1 To nRowCount
        nFieldData(iField, 1) = FIELDS(iField, "OFFSET")
        nFieldData(iField, 2) = FIELDS(iField, "LENGTH")
    Next
    
    Do While DATA.ROWCOUNT > 0
    
        For iRow = 1 To DATA.ROWCOUNT
        
            nTotalRecords = nTotalRecords + 1
            If Second(Now()) <> nCurSec Then ' And nCurRec <> rs.RecordCount Then
                nCurSec = Second(Now())
                nTotalSeconds = nTotalSeconds + 1
                nSecondsLeft = Int(((nTotalSeconds / iRow) * DATA.ROWCOUNT) * ((DATA.ROWCOUNT - iRow) / DATA.ROWCOUNT))
                
                RetVal = SysCmd(acSysCmdRemoveMeter)
                RetVal = SysCmd(acSysCmdInitMeter, "Importing " & strTableName & " from SAP, " & nSecondsLeft & " seconds remaining. [" & nTotalRecords + nRowSkip & " of " & DATA.ROWCOUNT + nRowSkip & "]", DATA.ROWCOUNT)
                RetVal = SysCmd(acSysCmdUpdateMeter, iRow)
                RetVal = DoEvents()
            End If

        
            strRow = DATA(iRow, 1) 'Pull this into a string rather than pull it from DATA each time...
            
            'Add a new row to the table
            rs.AddNew
            For iField = 1 To nRowCount
                rs.FIELDS(iField - 1).Value = Trim(Mid(strRow, nFieldData(iField, 1) + 1, nFieldData(iField, 2)))
            Next
            rs.Update
        Next
        
        '***********************************************************
        ' Check to see if the function should be called again . . .
        '***********************************************************
        If DATA.ROWCOUNT < nMaxRowCount Then
            'There is no need to read again to get a zero value.
            Exit Do
        Else
            'Move the offset by the block size.
            nRowSkip = nRowSkip + nMaxRowCount
        End If
        
        '***********************************************************
        ' Call the function again to get the next block of records.
        '***********************************************************
        ROWSKIPS.Value = nRowSkip
        ROWCOUNT.Value = nMaxRowCount
        MyFunc.Tables("DATA").Rows.RemoveAll 'Special thanks to Mike Burnett
        Result = MyFunc.CALL
        Set DATA = MyFunc.Tables("DATA")
        Set FIELDS = MyFunc.Tables("FIELDS")
        Set OPTIONS = MyFunc.Tables("OPTIONS")
        
    Loop
    
    rs.Close
    Close #2
    Set dbs = Nothing
    Set rs = Nothing
    RetVal = SysCmd(acSysCmdRemoveMeter)
    
    '*****************************************
    ' Log off of the SAP system and clean up.
    '*****************************************
    Set DATA = Nothing
    Set FIELDS = Nothing
    Set OPTIONS = Nothing
    Set MyFunc = Nothing
    R3.Connection.logoff
    Set R3 = Nothing
End Function

Update: Solving the SAPSQL_DATA_LOSS problem

This new error first seen in May of 2023 is caused by where statements being passed into the RFC with compare values that are longer than the length of the field in characters. See the bottom of this article about using RFCs to import table data into Microsoft Excel for more details and for suggestions on how to correct this issue.

Download

This function is included as part of Jimbo's VBA Toolbox which can be downloaded for free. It includes a lot of other great tools written over a 16-year career in data migration.

Programming Language: 
VBA