ODataをExcelから呼び出す|マクロでSAPのWeb APIを使う

前回の記事では、ABAPからSAPのODataを呼び出す記事を紹介した。

今回はExcelマクロ(VBA)からODataを呼び出す方法を、実際のVBAコードをベースに紹介したいと思う。

ODataそのものの基本は以下の記事で解説しているため、本記事では「VBAではどう書くか」に集中して話を進める。

目次

ブラウザからOData(Web API)を呼んで確認

ブラウザからURLでODataを呼ぶ

まず、サンプルで取り上げるODataサービス(Web API)を整理しておく。
ODataは前回の ODataをABAPから呼び出す方法 でも取り上げた C_PURCHASEORDER_FS_SRV だ。

たとえば、次のURLは C_PURCHASEORDER_FS_SRV を用いて、購買伝票の

  • 仕入先(Supplier)
  • 発注総額(PurchaseOrderNetAmount)
  • 発注日(PurchaseOrderDate)

を取得する。

https://s4hana2025.com:44300/sap/opu/odata/sap/C_PURCHASEORDER_FS_SRV/C_PurchaseOrderFs(‘4500000062‘)?$select=Supplier,PurchaseOrderNetAmount,PurchaseOrderDate

また、次のURLは上記に加えて、発注明細の情報も取り出す。

https://s4hana2025.com:44300/sap/opu/odata/sap/C_PURCHASEORDER_FS_SRV/C_PurchaseOrderFs(‘4500000062‘)?$select=Supplier,PurchaseOrderNetAmount,PurchaseOrderDate,to_PurchaseOrderItem&$expand=to_PurchaseOrderItem

このURLをブラウザで叩くと、次のようなXMLの結果が返ってくる。

<entry xmlns="http://www.w3.org/2005/Atom" xmlns:m="http://schemas.microsoft.com/ado/2007/08/dataservices/metadata" xmlns:d="http://schemas.microsoft.com/ado/2007/08/dataservices" xml:base="https://S4HANA2025.COM:44300/sap/opu/odata/sap/C_PURCHASEORDER_FS_SRV/">
  <id>https://S4HANA2025.COM:44300/sap/opu/odata/sap/C_PURCHASEORDER_FS_SRV/C_PurchaseOrderFs('4500000062')</id>
  <title type="text">C_PurchaseOrderFs('4500000062')</title>
  <updated>2026-04-06T14:25:49Z</updated>
  <category term="C_PURCHASEORDER_FS_SRV.C_PurchaseOrderFsType" scheme="http://schemas.microsoft.com/ado/2007/08/dataservices/scheme"/>
  <link href="C_PurchaseOrderFs('4500000062')" rel="self" title="C_PurchaseOrderFsType"/>
  <link href="C_PurchaseOrderFs('4500000062')/to_PurchaseOrderItem" rel="http://schemas.microsoft.com/ado/2007/08/dataservices/related/to_PurchaseOrderItem" type="application/atom+xml;type=feed" title="to_PurchaseOrderItem">
    <m:inline>
      <feed xml:base="https://S4HANA2025.COM:44300/sap/opu/odata/sap/C_PURCHASEORDER_FS_SRV/">
        <id>https://S4HANA2025.COM:44300/sap/opu/odata/sap/C_PURCHASEORDER_FS_SRV/C_PurchaseOrderFs('4500000062')/to_PurchaseOrderItem</id>
        <title type="text">C_PurOrdItemEnh</title>
        <updated>2026-04-06T14:25:49Z</updated>
        <author>
          <name/>
        </author>
        <link href="C_PurchaseOrderFs('4500000062')/to_PurchaseOrderItem" rel="self" title="C_PurOrdItemEnh"/>
        <entry>
          <id>https://S4HANA2025.COM:44300/sap/opu/odata/sap/C_PURCHASEORDER_FS_SRV/C_PurOrdItemEnh(PurchaseOrder='4500000062',PurchaseOrderItem='00010')</id>
          <title type="text">C_PurOrdItemEnh(PurchaseOrder='4500000062',PurchaseOrderItem='00010')</title>
          <updated>2026-04-06T14:25:49Z</updated>
          <category term="C_PURCHASEORDER_FS_SRV.C_PurOrdItemEnhType" scheme="http://schemas.microsoft.com/ado/2007/08/dataservices/scheme"/>
          <link href="C_PurOrdItemEnh(PurchaseOrder='4500000062',PurchaseOrderItem='00010')" rel="self" title="C_PurOrdItemEnhType"/>
          <link href="C_PurOrdItemEnh(PurchaseOrder='4500000062',PurchaseOrderItem='00010')/to_PlantValHelp" rel="http://schemas.microsoft.com/ado/2007/08/dataservices/related/to_PlantValHelp" type="application/atom+xml;type=entry" title="to_PlantValHelp"/>
          <link href="C_PurOrdItemEnh(PurchaseOrder='4500000062',PurchaseOrderItem='00010')/to_POAccountAssignmentFactSheet" rel="http://schemas.microsoft.com/ado/2007/08/dataservices/related/to_POAccountAssignmentFactSheet" type="application/atom+xml;type=feed" title="to_POAccountAssignmentFactSheet"/>
          <link href="C_PurOrdItemEnh(PurchaseOrder='4500000062',PurchaseOrderItem='00010')/to_PODeliveryAddressFactSheet" rel="http://schemas.microsoft.com/ado/2007/08/dataservices/related/to_PODeliveryAddressFactSheet" type="application/atom+xml;type=entry" title="to_PODeliveryAddressFactSheet"/>
          <link href="C_PurOrdItemEnh(PurchaseOrder='4500000062',PurchaseOrderItem='00010')/to_POScheduleLineFactSheet" rel="http://schemas.microsoft.com/ado/2007/08/dataservices/related/to_POScheduleLineFactSheet" type="application/atom+xml;type=feed" title="to_POScheduleLineFactSheet"/>
          <link href="C_PurOrdItemEnh(PurchaseOrder='4500000062',PurchaseOrderItem='00010')/to_PurchaseOrderEnhanced" rel="http://schemas.microsoft.com/ado/2007/08/dataservices/related/to_PurchaseOrderEnhanced" type="application/atom+xml;type=entry" title="to_PurchaseOrderEnhanced"/>
          <link href="C_PurOrdItemEnh(PurchaseOrder='4500000062',PurchaseOrderItem='00010')/to_PurOrdScheduleLine" rel="http://schemas.microsoft.com/ado/2007/08/dataservices/related/to_PurOrdScheduleLine" type="application/atom+xml;type=feed" title="to_PurOrdScheduleLine"/>
          <link href="C_PurOrdItemEnh(PurchaseOrder='4500000062',PurchaseOrderItem='00010')/to_ServicePerformer" rel="http://schemas.microsoft.com/ado/2007/08/dataservices/related/to_ServicePerformer" type="application/atom+xml;type=entry" title="to_ServicePerformer"/>
          <content type="application/xml">
            <m:properties>
              <d:PurchaseOrder>4500000062</d:PurchaseOrder>
              <d:PurchaseOrderItem>00010</d:PurchaseOrderItem>
              <d:PurchaseOrderItemCategory>0</d:PurchaseOrderItemCategory>
              <d:PurchaseOrderItemCategory_Text>標準</d:PurchaseOrderItemCategory_Text>
              <d:Material>RM233-2</d:Material>
              <d:ManufacturerMaterial>RM233-2</d:ManufacturerMaterial>
              <d:ManufacturerMaterial_Text>RAW233-2、ND、かんばん、分納契約</d:ManufacturerMaterial_Text>
              <d:MaterialGroup>L002</d:MaterialGroup>
              <d:MaterialGroup_Text>原材料</d:MaterialGroup_Text>
              <d:Plant>1710</d:Plant>
              <d:Plant_Text>Plant 1 US</d:Plant_Text>
              <d:AccountAssignmentCategory/>
              <d:AccountAssignmentCategory_Text/>
              <d:PurchaseOrderItemText>RAW233-2,ND,KANBAN,sched. agreements</d:PurchaseOrderItemText>
              <d:PurchaseRequisition/>
              <d:PurchaseRequisitionItem>00000</d:PurchaseRequisitionItem>
              <d:ProductType/>
              <d:ProductType_Text>互換モード</d:ProductType_Text>
              <d:FirstDeliveryDate>2017-10-31T00:00:00</d:FirstDeliveryDate>
              <d:OrderQuantity>100</d:OrderQuantity>
              <d:PurchaseOrderQuantityUnit>PC</d:PurchaseOrderQuantityUnit>
              <d:DocumentCurrency>USD</d:DocumentCurrency>
              <d:PurchaseOrderPriceUnit>PC</d:PurchaseOrderPriceUnit>
              <d:NetPriceAmount>3.00</d:NetPriceAmount>
              <d:PurchaseOrderNetPriceQuantity>1</d:PurchaseOrderNetPriceQuantity>
              <d:NetAmount>300.00</d:NetAmount>
              <d:PurchasingDocumentDeletionCode/>
              <d:ServicePerformer/>
              <d:PerformancePeriodStartDate m:null="true"/>
              <d:PerformancePeriodEndDate m:null="true"/>
              <d:PurchaseOrderItemStatus/>
              <d:OverdelivTolrtdLmtRatioInPct>10.0</d:OverdelivTolrtdLmtRatioInPct>
              <d:UnderdelivTolrtdLmtRatioInPct>10.0</d:UnderdelivTolrtdLmtRatioInPct>
              <d:UnlimitedOverdeliveryIsAllowed>false</d:UnlimitedOverdeliveryIsAllowed>
              <d:IsToBeAcceptedAtOrigin>false</d:IsToBeAcceptedAtOrigin>
              <d:StorageLocation>171E</d:StorageLocation>
              <d:IntrastatServiceCode/>
              <d:CommodityCode/>
              <d:ShippingInstruction/>
              <d:IncotermsVersion/>
              <d:IncotermsClassification>EXW</d:IncotermsClassification>
              <d:IncotermsLocation1>VENDOR</d:IncotermsLocation1>
              <d:IncotermsLocation2/>
              <d:PurchasingInfoRecord>5300000154</d:PurchasingInfoRecord>
              <d:PurchaseContract/>
              <d:PurContractForOverallLimit/>
              <d:PurchaseContractItem>00000</d:PurchaseContractItem>
              <d:InfoRecordIsToBeUpdated>true</d:InfoRecordIsToBeUpdated>
              <d:TaxCode>I1</d:TaxCode>
              <d:TaxCode_Text>A/P 売上税、課税対象、明細に配賦</d:TaxCode_Text>
              <d:TaxJurisdiction>7700000000</d:TaxJurisdiction>
              <d:TaxCalculationProcedure>TAXUSJ</d:TaxCalculationProcedure>
              <d:SupplierConfirmationControlKey/>
              <d:PurgDocOrderAcknNumber/>
              <d:IsOrderAcknRqd>false</d:IsOrderAcknRqd>
              <d:ItemIsRejectedBySupplier>false</d:ItemIsRejectedBySupplier>
              <d:RequisitionerName/>
            </m:properties>
          </content>
        </entry>
      </feed>
    </m:inline>
  </link>
  <content type="application/xml">
    <m:properties>
      <d:PurchaseOrderDate>2017-10-26T00:00:00</d:PurchaseOrderDate>
      <d:Supplier>17300002</d:Supplier>
      <d:PurchaseOrderNetAmount>300.00</d:PurchaseOrderNetAmount>
    </m:properties>
  </content>
</entry>

Excelマクロ(VBA)でURLを送信した場合も、上記と同じXMLがレスポンスとして返ってくる。

以下では、これと同じことをVBAで実装する例を紹介する。

ExcelマクロからODataを呼び出す

作成したExcelマクロ(VBA)

今回作成したサンプルVBAコードの全容は以下である。

Option Explicit
'===========================================================
' 購買発注ヘッダと発注明細 00010 を、1本の OData URL で取得する処理
'
' 取得対象
'   【ヘッダ】
'   - Supplier
'   - PurchaseOrderNetAmount
'   - PurchaseOrderDate
'
'   【明細 00010】
'   - Material
'   - OrderQuantity
'   - PurchaseOrderQuantityUnit
'
' 出力先
'   Main!B3 : Supplier
'   Main!B4 : PurchaseOrderNetAmount
'   Main!B5 : PurchaseOrderDate
'   Main!B6 : Material               (明細00010)
'   Main!B7 : OrderQuantity          (明細00010)
'   Main!B8 : PurchaseOrderQuantityUnit(明細00010)
'
' 入力元
'   Main!B2   : 購買発注番号
'   Config!B1 : SAP のベースURL
'   Config!B2 : sap-client
'   Config!B3 : ユーザID
'   Config!B4 : パスワード
'
' ポイント
'   - URLは1本だけ呼び出す
'   - 明細は to_PurchaseOrderItem で展開して全部受け取る
'   - その後、VBA側で PurchaseOrderItem = "00010" を探す
'===========================================================
Public Sub GetPOHeaderAndItem00010_FromSAP()

    '--- ワークシート
    Dim wsMain As Worksheet
    Dim wsConfig As Worksheet

    '--- 入力値・接続情報
    Dim purchaseOrder As String
    Dim baseUrl As String
    Dim sapClient As String
    Dim userId As String
    Dim password As String

    '--- 通信用URL
    Dim requestUrl As String

    '--- HTTP通信用オブジェクト
    Dim http As Object

    '--- XML解析用オブジェクト
    Dim xmlDoc As Object

    '--- ヘッダ用ノード
    Dim supplierNode As Object
    Dim netAmountNode As Object
    Dim orderDateNode As Object

    '--- 明細00010の entry ノード
    Dim itemEntryNode As Object

    '--- 明細00010の項目ノード
    Dim materialNode As Object
    Dim orderQtyNode As Object
    Dim orderUnitNode As Object

    '--- 出力前に値をいったん文字列で保持する
    Dim supplierText As String
    Dim netAmountText As String
    Dim orderDateText As String
    Dim materialText As String
    Dim orderQtyText As String
    Dim orderUnitText As String

    ' 何かエラーが出た場合は ErrHandler に飛ぶ
    On Error GoTo ErrHandler

    '===========================================================
    ' 1. シートを取得する
    '===========================================================
    Set wsMain = ThisWorkbook.Worksheets("Main")
    Set wsConfig = ThisWorkbook.Worksheets("Config")

    '===========================================================
    ' 2. 画面・設定シートから必要な値を読み込む
    '===========================================================
    purchaseOrder = Trim(CStr(wsMain.Range("B2").Value))
    baseUrl = Trim(CStr(wsConfig.Range("B1").Value))
    sapClient = Trim(CStr(wsConfig.Range("B2").Value))
    userId = Trim(CStr(wsConfig.Range("B3").Value))
    password = Trim(CStr(wsConfig.Range("B4").Value))

    '===========================================================
    ' 3. 必須入力をチェックする
    '    ここで止まる場合は、まずセルの入力値を見直す
    '===========================================================
    If purchaseOrder = "" Then
        Err.Raise vbObjectError + 100, , "Main シート B2 の購買発注番号が空である。"
    End If

    If baseUrl = "" Then
        Err.Raise vbObjectError + 101, , "Config シート B1 のベースURLが空である。"
    End If

    If sapClient = "" Then
        Err.Raise vbObjectError + 102, , "Config シート B2 の sap-client が空である。"
    End If

    If userId = "" Then
        Err.Raise vbObjectError + 103, , "Config シート B3 のユーザIDが空である。"
    End If

    If password = "" Then
        Err.Raise vbObjectError + 104, , "Config シート B4 のパスワードが空である。"
    End If

    '===========================================================
    ' 4. 1本で取得する OData URL を組み立てる
    '
    '    ここではヘッダ項目を $select しつつ、
    '    明細を $expand=to_PurchaseOrderItem で展開する
    '
    '    注意:
    '    この環境では $expand の中に ($select=...) を付けると
    '    エラーになるため、to_PurchaseOrderItem はそのまま展開する
    '===========================================================
    requestUrl = baseUrl & _
        "/sap/opu/odata/sap/C_PURCHASEORDER_FS_SRV/" & _
        "C_PurchaseOrderFs('" & Replace(purchaseOrder, "'", "''") & "')" & _
        "?$select=Supplier,PurchaseOrderNetAmount,PurchaseOrderDate,to_PurchaseOrderItem" & _
        "&$expand=to_PurchaseOrderItem" & _
        "&sap-client=" & sapClient

    ' Immediate ウィンドウへURLを出力する
    ' VBAエディタで Ctrl + G を押すと確認できる
    Debug.Print "===== Request URL ====="
    Debug.Print requestUrl

    '===========================================================
    ' 5. SAP へ HTTP GET を送信する
    '===========================================================
    Set http = CreateObject("MSXML2.XMLHTTP.6.0")

    ' False は同期実行である
    ' 返答が返るまで処理が止まるため、初心者には追いやすい
    http.Open "GET", requestUrl, False, userId, password

    ' XMLで返してほしいことを伝える
    http.setRequestHeader "Accept", "application/atom+xml,application/xml"

    ' リクエスト送信
    http.Send

    ' ステータスコードを確認する
    Debug.Print "===== HTTP Status ====="
    Debug.Print http.Status & " " & http.StatusText

    ' レスポンス全文は長いことがあるため、先頭だけ確認する
    Debug.Print "===== Response (first 2000 chars) ====="
    Debug.Print Left$(http.ResponseText, 2000)

    If http.Status <> 200 Then
        Err.Raise vbObjectError + 1, , "HTTP Error: " & http.Status & " " & http.StatusText
    End If

    '===========================================================
    ' 6. XML文字列を DOM として読み込む
    '===========================================================
    Set xmlDoc = CreateObject("MSXML2.DOMDocument.6.0")
    xmlDoc.async = False

    If Not xmlDoc.LoadXML(http.ResponseText) Then
        Err.Raise vbObjectError + 105, , "XMLの解析に失敗した。ResponseText を確認すること。"
    End If

    ' XPath を使う設定
    xmlDoc.SetProperty "SelectionLanguage", "XPath"

    ' OData Atom XML で使う名前空間を設定する
    ' atom を入れないと entry / content が取れない
    xmlDoc.SetProperty "SelectionNamespaces", _
        "xmlns:atom='http://www.w3.org/2005/Atom' " & _
        "xmlns:m='http://schemas.microsoft.com/ado/2007/08/dataservices/metadata' " & _
        "xmlns:d='http://schemas.microsoft.com/ado/2007/08/dataservices'"

    '===========================================================
    ' 7. ヘッダ項目を取得する
    '
    '    単一エンティティのため、ルートの atom:entry 配下に
    '    ヘッダの m:properties がある
    '===========================================================
    Set supplierNode = xmlDoc.SelectSingleNode("/atom:entry/atom:content/m:properties/d:Supplier")
    Set netAmountNode = xmlDoc.SelectSingleNode("/atom:entry/atom:content/m:properties/d:PurchaseOrderNetAmount")
    Set orderDateNode = xmlDoc.SelectSingleNode("/atom:entry/atom:content/m:properties/d:PurchaseOrderDate")

    If supplierNode Is Nothing Then
        Err.Raise vbObjectError + 2, , "Supplier が見つからない。"
    End If

    If netAmountNode Is Nothing Then
        Err.Raise vbObjectError + 3, , "PurchaseOrderNetAmount が見つからない。"
    End If

    If orderDateNode Is Nothing Then
        Err.Raise vbObjectError + 4, , "PurchaseOrderDate が見つからない。"
    End If

    supplierText = supplierNode.Text
    netAmountText = netAmountNode.Text
    orderDateText = orderDateNode.Text

    '===========================================================
    ' 8. 展開された明細の中から、明細 00010 の entry を探す
    '
    '    構造のイメージ
    '    entry(ヘッダ)
    '      └ link title="to_PurchaseOrderItem"
    '         └ m:inline
    '            └ feed
    '               └ entry(明細1件ずつ)
    '
    '    その中で d:PurchaseOrderItem = "00010" の entry を探す
    '===========================================================
    Set itemEntryNode = xmlDoc.SelectSingleNode( _
        "/atom:entry/atom:link[@title='to_PurchaseOrderItem']/m:inline/atom:feed/atom:entry" & _
        "[atom:content/m:properties/d:PurchaseOrderItem='00010']")

    If itemEntryNode Is Nothing Then
        Err.Raise vbObjectError + 5, , "明細 00010 が見つからない。"
    End If

    '===========================================================
    ' 9. 明細 00010 から必要項目を取得する
    '===========================================================
    Set materialNode = itemEntryNode.SelectSingleNode("atom:content/m:properties/d:Material")
    Set orderQtyNode = itemEntryNode.SelectSingleNode("atom:content/m:properties/d:OrderQuantity")
    Set orderUnitNode = itemEntryNode.SelectSingleNode("atom:content/m:properties/d:PurchaseOrderQuantityUnit")

    If materialNode Is Nothing Then
        Err.Raise vbObjectError + 6, , "明細 00010 の Material が見つからない。"
    End If

    If orderQtyNode Is Nothing Then
        Err.Raise vbObjectError + 7, , "明細 00010 の OrderQuantity が見つからない。"
    End If

    If orderUnitNode Is Nothing Then
        Err.Raise vbObjectError + 8, , "明細 00010 の PurchaseOrderQuantityUnit が見つからない。"
    End If

    materialText = materialNode.Text
    orderQtyText = orderQtyNode.Text
    orderUnitText = orderUnitNode.Text

    '===========================================================
    ' 10. 取得した値をシートへ出力する
    '===========================================================
    wsMain.Range("B3").Value = supplierText
    wsMain.Range("B4").Value = netAmountText
    wsMain.Range("B5").Value = orderDateText
    wsMain.Range("B6").Value = materialText
    wsMain.Range("B7").Value = orderQtyText
    wsMain.Range("B8").Value = orderUnitText

    '===========================================================
    ' 11. デバッグ確認用に取得結果を出力する
    '===========================================================
    Debug.Print "===== Result ====="
    Debug.Print "Supplier = " & supplierText
    Debug.Print "PurchaseOrderNetAmount = " & netAmountText
    Debug.Print "PurchaseOrderDate = " & orderDateText
    Debug.Print "Material(00010) = " & materialText
    Debug.Print "OrderQuantity(00010) = " & orderQtyText
    Debug.Print "PurchaseOrderQuantityUnit(00010) = " & orderUnitText

    MsgBox "ヘッダと明細 00010 の取得が完了した。", vbInformation, "GetPOHeaderAndItem00010_FromSAP"
    Exit Sub

'===========================================================
' 12. エラーハンドリング
'    エラー時は Immediate ウィンドウとメッセージで内容を出す
'===========================================================
ErrHandler:
    Debug.Print "===== Error ====="
    Debug.Print "Err.Number = " & Err.Number
    Debug.Print "Err.Description = " & Err.Description

    MsgBox Err.Description, vbExclamation, "GetPOHeaderAndItem00010_FromSAP"

End Sub

上記マクロを実装したExcelファイルは資料室にも公開してあるので、必要に応じでダウンロードし、利用してほしい。

なお、筆者が検証に使用した環境は、以前の記事で紹介した Professor’s RSA である。

このVBAコードがやっていること

このVBAは、SAPのODataサービスに対して1回だけアクセスし、購買発注のヘッダ情報と明細00010の情報をまとめて取得する処理である。

全体的な流れは、以下の通りだ。

  1. Excelシートから購買発注番号や接続情報を読む
  2. ODataのURLを組み立てる
  3. SAPにHTTP GETで問い合わせる
  4. 返ってきたXMLを読む
  5. ヘッダ情報を取り出す
  6. 明細一覧の中から「00010」の行だけ探す
  7. 結果をExcelに出力

$select $expand

今回のURLでポイントになるのが、ODataの$select$expandだ。

$Selectは「欲しい項目を絞る」ための指定$expandは「関連データも一緒に広げて取る」ための指定である。

ODataの$select$expandを使って、購買発注ヘッダ(Supplierなど)と明細(to_PurchaseOrderItem)を1回の通信でまとめて取得している。

requestUrl = baseUrl & _
    "/sap/opu/odata/sap/C_PURCHASEORDER_FS_SRV/" & _
    "C_PurchaseOrderFs('" & Replace(purchaseOrder, "'", "''") & "')" & _
    "?$select=Supplier,PurchaseOrderNetAmount,PurchaseOrderDate,to_PurchaseOrderItem" & _
    "&$expand=to_PurchaseOrderItem" & _
    "&sap-client=" & sapClient

VBAでODataのデバッグをやるときのコツ

1. まず確認すべきは「送ったURL」

ODataのデバッグでは、最初に見るべきはURLである。
URLの組み立てでミスすると、正常なレスポンスを得られない。

そのため、サンプルコードでは通信前に requestUrlDebug.Print で出力している。

そしていきなり完成形の長いURLを使うのではなく、短いURLから段階的に確認するのがコツである。

たとえば、次のように進めるとよい。

  1. ODataサービスのルートにアクセスする
  2. エンティティ単体(キー指定のみ)で取得する
  3. $select を追加する
  4. $expand を追加する

このように少しずつ条件を足していくことで、どの段階でエラーになるかがはっきりする。

「サービスのルートにアクセスする」とは、ODataサービスの一番上(入口)に対して、何も条件を付けずにアクセスすることである。

たとえば今回の例なら、URLはこうなる。

https://<HOST>/sap/opu/odata/sap/C_PURCHASEORDER_FS_SRV/

この状態では、
  • $select も付けない
  • $expand も付けない
  • キー指定(購買発注番号)もしない

つまり、一番シンプルなURLである。

また、作成したURLはVBAの中だけで確認せず、ブラウザで直接実行するのも有効である。
ブラウザでアクセスして、

  • エラーが出ないか
  • HTTP 200で返るか
  • 欲しい項目がちゃんと含まれているか

を確認する。

2. HTTPステータスを見て成功かエラーかを判定

httpでURLを送信するとレスポンスが返ってくるが、レスポンス本文を見るよりも先に、httpのStatus を見る。

サンプルコードでは Status を見るため、リクエスト送信後に StatusStatusTextDebug.Print で出力している。

Debug.Print http.Status
Debug.Print http.StatusText

Status で成功かエラーを切り分けられる。

Statusによる成功/エラーの判定

  • 200 : 通信成功
  • 401 : 認証エラー
  • 404 : URLやサービス名の誤り
  • 500 : SAP側エラーかクエリ条件不正

3. レスポンス全文ではなく、先頭だけを見る

レスポンス文が長いと読みにくい。
そのため、最初はレスポンスの先頭だけ確認すると効率がよい。

Debug.Print Left$(http.ResponseText, 1000)

ここで見たいのは、次のような点である。

  • 本当にXMLが返っているか
  • HTMLのエラーページが返っていないか
  • SAPのエラーメッセージが返っていないか

4. XML解析エラーと通信エラーは分けて考える

通信が成功しても、XMLとして読めるとは限らない。
そのため、LoadXML できるかは別で確認するのがよい。

If Not xmlDoc.LoadXML(http.ResponseText) Then
    Debug.Print "XML parse error"
End If

ここで失敗するなら、問題はWeb通信ではなく、返却内容の形式にある可能性が高い。
この切り分けをしないと、原因を見誤りやすい。

5. ODataでは名前空間を真っ先に疑う

レスポンスに値が見えているのに SelectSingleNode で取れないときは、まず名前空間を疑うべきである。

xmlDoc.SetProperty "SelectionNamespaces", _
    "xmlns:atom='http://www.w3.org/2005/Atom' " & _
    "xmlns:m='http://schemas.microsoft.com/ado/2007/08/dataservices/metadata' " & _
    "xmlns:d='http://schemas.microsoft.com/ado/2007/08/dataservices'"

ODataのAtom XMLでは、entrycontent や項目名にプレフィックスが付く。
この設定がないと、データがあっても取れない。

6. XPathは一発で決めず、短く分けて試す

xmlDoc.SetProperty "SelectionLanguage", "XPath"

はXML文書の要素検索を「XPath」で行うことを宣言するため、プロパティ SelectionLanguage にセットしている。

長いXPathをいきなり書くと、どこで失敗したか分かりにくい。
そのため、短く刻んで確認するのがコツである。

たとえば、

Debug.Print xmlDoc.SelectSingleNode("/atom:entry").XML

次に

Debug.Print xmlDoc.SelectSingleNode("/atom:entry/atom:link").XML

というように、少しずつ下へたどる。
どこまでは取れて、どこから取れないかを見つけるのが重要である。

7. $expand を使ったときは、まず構造を見る

発注明細を取得するため $expand を使っているが、$expandで取得できる子データは入れ子構造で返る。
そのため、いきなり条件付きで探すのではなく、まず明細全体の構造を見るようにする。

つまり、

  • link
  • m:inline
  • feed
  • entry

の順に存在を確認する。
明細が見つからないときは、条件式より先に構造の理解不足を疑うのが正しい。

8. .Text の前に “Is Nothing” で確認する

これは小さいが重要である。

If supplierNode Is Nothing Then
    Debug.Print "Supplier not found"
Else
    Debug.Print supplierNode.Text
End If

いきなり .Text を取ると、エラーは出るが原因が雑になる。
Is Nothing を入れると、「取れない」のか「空文字」なのかを区別できる。

9. ログは値だけでなく、ラベル付きで出す

デバッグ出力は、値だけだと判別できない。そのため、必ずラベルを付けるのがよい。

Debug.Print "Status=" & http.Status
Debug.Print "URL=" & requestUrl
Debug.Print "Supplier=" & supplierText

これだけで、調査のしやすさがかなり変わる。

マクロは証明書のエラーとは無縁?

ABAPではSSL証明書のエラーでハマりやすい。
証明書が正しくインポートされていないと、実行時に SSSLERR_PEER_CERT_UNTRUSTED エラーが発生してしまう。
前回の記事でも、SSSLERR_PEER_CERT_UNTRUSTED  エラーの対策、すなわち ABAPコーディング以上に、SSL証明書のインポート手順など「証明書」への対応に時間を割いた。

しかし、Excelマクロ実行時に SSSLERR_PEER_CERT_UNTRUSTED エラーは起きない。
というか、実際にマクロを動かしてみれば分かる通り、SSL証明書はほぼ意識しない。

この違いは、「誰が証明書をチェックしているか」の違いである。

ABAPの場合:SAPサーバがチェックする

ABAPでODataを呼ぶとき、通信しているのはユーザのPCではなくSAPサーバである。
つまり、

  • SAPアプリケーションサーバがHTTPS通信する
  • SAP自身が証明書を検証する
  • SAPが「この接続は安全か?」を判断する

このように、SAP(ABAP)はWindowsの証明書ストアを使っていない
そのため、SAP側で以下が必要になる。

  • T-CD: STRUSTで証明書(CA)を登録
  • SSLクライアントPSEの設定
  • SM59でHTTPS接続設定

これがないと、「このサーバは信用できない!」となってエラーになる。

Excelマクロの場合:Windowsがチェックする

一方、Excelマクロ(VBA)はどうか?

内部的にはこうだ。

  • Windowsの通信機能(WinHTTP / WinINet)を使う
  • SSLの検証はWindowsがやる

つまり、Excel自身は証明書を意識していない ということである。

さらにWindows内部では、以下も実行される。

  • 主要な認証局(CA)を最初から信頼済み
  • 自動更新もされる

その結果、大抵のHTTPSサイトなら、証明書のエラーは発生することなく、だいたい通るのである。

証明書のエラーを切り分けるなら

上記の通り、ABAPと違ってExcelマクロは証明書エラーの影響を受け難い。

この性質を利用すれば、

Excelマクロで成功 

という順番でデバッグすれば、失敗の原因は証明書という切り分けが可能だ。

まとめ:ABAP+VBAでデバッグの生産性を上げる

前回記事で紹介した、ABAPからODataを呼ぶコードを、Excelマクロに置き換えたVBAコードとして紹介した。

ABAPではハマりやすい証明書エラーは、Excelマクロではほとんど起きない。
この理由は、ABAPは証明書のチェックをSAPサーバ側で行うのに対し、Excelでは証明書のチェックをWindowsが一括で面倒を見てくれるからである。

このため、Web API 時代のSAPエンジニアは、ABAP/VBAの両スキルを持つことが理想だ。

「ABAPは書けるけどVBAは書いたことがない」

という人は一定数いると思う。でも、安心してほしい。

今は生成AIの時代でもある。生成AIにABAPコードを読ませ、「これと同じことをExcelマクロで書いてほしい」と伝えるだけで、AIがVBAコードを生成してくれるのだ。


よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

コメント

コメントする

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください

目次