UiPath から SAP S/4HANA の OData を呼び出す - その２ UiPath で実装する

## はじめに
[前回の記事](https://qiita.com/MasahiroHarada1105/items/f3d92f7d54de40b67f47
)の続きです。今回はいよいよ UiPath から S/4HANA の OData API ``/A_PurchaseOrder`` を呼び出してみます。

本記事で作成するワークフローは Studio 23.10.4 で作成しています。

## SAP OData API 呼び出しで知っておきたいこと
前回は、内部（SAP GUI）から OData API を呼び出しましたが、今回は外部（UiPath）からの呼び出しとなります。外部から OData API を呼び出す場合は下記2点を考慮する必要があります。

**1. 認証が必要**
**2. GET, HEAD 以外の呼び出し (POST, PUT, PATCH, MERGE, DELETE) には CSRF トークンが必要**

それぞれ見ていきましょう。

### 1. 認証が必要
当然と言えば当然ですが、API 呼び出しには認証が必要になります。[SAP Business Accelerator Hub の該当 API ページ](https://api.sap.com/api/OP_API_PURCHASEORDER_PROCESS_SRV_0001/overview?ReleaseInfo=2020%20FPS02) の Authentication Methods を見るとどの認証方法を使えるか確認できます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3322296/05dbdae0-c42e-27d1-afce-49303d6b59f9.png)

今回は Basic Authentication（Basic 認証）を使います。

認証が通るとセッションIDが払い出されます。後続の API 呼び出し時に払い出されたセッションIDを付与する必要があります。

### 2. GET, HEAD 以外の呼び出し (POST, PUT, PATCH, MERGE, DELETE) には CSRF トークンが必要
CSRF トークンを取得するには、リクエスト HTTP ヘッダーに ``x-csrf-token : Fetch`` を含めて GET または HEAD メソッドで API を呼び出す必要があります。

呼び出しに成功すると、レスポンス HTTP ヘッダーの ``x-csrf-token`` に CSRF トークンが設定されるので取得します。以降の GET 以外の呼び出し時にこの CSRF トークンをリクエストヘッダーに設定します。

https://help.sap.com/docs/odata-provisioning/odata-provisioning/d2e706bd63c043c7b7ae1d2dc830bb79.html?locale=ja-JP

https://community.sap.com/t5/technology-blogs-by-sap/how-does-csrf-token-work-sap-gateway/ba-p/13520186

## ワークフローを実装する

### 依存関係の追加
UiPath から S/4HANA の OData API を呼び出しは、``HTTP 要求`` アクティビティを使います。このアクティビティは ``UiPath.WebAPI.Activities`` に含まれています。

プロジェクトを新規に作成して、プロジェクトの依存関係に追加します。

https://docs.uipath.com/ja/activities/other/latest/developer/about-the-web-activities-pack

### セッション ID と CSRF トークンの取得

POST の ``/A_PurchaseOrder`` の呼び出しで必要となるセッション ID と CSRF トークンを取得します。専用エンドポイントは用意されていないので、今回は GET で ``/A_PurchaseOrder`` を呼び出してエンドポイントのメタデータを取得することでトークンを取得します。

呼び出すエンドポイントの URI は以下となります。クエリパラメータとして ``$metadata`` を付けることでエンドポイントのメタデータが取得できます。
```
https://{host}:{port}/sap/opu/odata/sap/API_PURCHASEORDER_PROCESS_SRV/A_PurchaseOrder?$metadata
```

はじめに、以下の変数を定義します。
<table>
  <thead>
    <tr>
      <th>変数</th><th>型</th>
    </tr>
  </thead>
  <tr>
    <td>strUri</td>
    <td>String</td>
  </tr>
  <tr>
    <td>strClient</td>
    <td>String</td>
  </tr>
  <tr>
    <td>disResHeader</td>
    <td>Dictionary<Of String, String></td>
  </tr>
  <tr>
    <td>strSapSessionId</td>
    <td>String</td>
  </tr>
  <tr>
    <td>strCsrfToken</td>
    <td>String</td>
  </tr>
  <tr>
    <td>regexResult</td>
    <td>IEnumerable&lt;Match&gt;</td>
  </tr>
  <tr>
    <td>strResContent</td>
    <td>String</td>
  </tr>
  <tr>
    <td>intStsCode</td>
    <td>int32</td>
  </tr>
  <tr>
    <td>jsonResContent</td>
    <td>JObject</td>
  </tr>
</table>

``strUri`` と ``strClient`` に ``代入`` アクティビティで値を指定します。
<table>
  <thead>
    <tr>
      <th>変数</th><th>値</th>
    </tr>
  </thead>
  <tr>
    <td>strUri</td>
    <td>
      https://{host}:{port}/sap/opu/odata/sap/API_PURCHASEORDER_PROCESS_SRV/A_PurchaseOrder<br />
      <br />
      {host} {port} は実際の値を指定します。
    </td>
  </tr>
  <tr>
    <td>strClient</td>
    <td>クライアントID</td>
  </tr>
</table>

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3322296/04e632df-8ff8-2084-7306-23efd05a4e72.png)

続いて ``HTTP 要求`` アクティビティを配置します。以下のプロパティを指定します。

<table>
  <thead>
    <tr>
      <th>プロパティ</th><th>値</th>
    </tr>
  </thead>
  <tr>
    <td>エンドポイント</td>
    <td>strUri + "?$metadata"</td>
  </tr>
  <tr>
    <td>要求メソッド</td>
    <td>GET</td>
  </tr>
  <tr>
    <td>HTTP ヘッダー</td>
    <td>
      sap-client : strClient<br />
      x-csrf-token : "Fetch"
    </td>
  </tr>
  <tr>
    <td>認証</td>
    <td>
      簡易 HTTP<br />
      ユーザー名、パスワード
    </td>
  </tr>
  <tr>
    <td>出力 - ヘッダー</td>
    <td>dicResHeader</td>
  </tr>
  <tr>
    <td>応答コンテンツ</td>
    <td>strResContent</td>
  </tr>
  <tr>
    <td>応答ステータス</td>
    <td>intStsCode</td>
  </tr>
</table>

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3322296/45366831-988b-b8d5-98e5-e0bd24dd20e3.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3322296/db8aac10-d87c-d744-660c-5f1bea2ac80b.png)

エンドポイントを呼び出したら、レスポンスヘッダーから CSRF トークンを取得します。CSRF トークンはレスポンスヘッダーに ``x-csrf-token`` として格納されています。

:::note info
``HTTP 要求`` アクティビティでの API 呼び出しがうまくいかないときは、 プロパティ ``応答コンテンツ`` と ``応答ステータス`` に設定された値を確認すると、呼び出しの失敗原因を特定できる可能性があるので活用してください。
:::

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3322296/8ec527c6-94c3-1a5f-09f0-3cb2fc9b54b4.png)

````vb:代入イメージ
strCsrfToken = dicResHeader("x-csrf-token")
````

続いて、レスポンスヘッダーからセッション ID を取得します。セッション ID はレスポンスヘッダーに含まれる Cookie で ``SAP_SESSIONID_<sid>_<client>`` という形式のキーに格納されています。

``一致する文字列を取得`` アクティビティを使ってセッション ID を取得します。このアクティビティは正規表現を使って文字列を抽出することが出来ます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3322296/63ddd65c-0e3a-f30c-7ed8-5cbbc240d6e3.png)

プロパティはこちらです。
<table>
  <thead>
    <tr>
      <th>プロパティ</th><th>値</th>
    </tr>
  </thead>
  <tr>
    <td>結果</td>
    <td>regexResult</td>
  </tr>
  <tr>
    <td>パターン</td>
    <td>SAP_SESSIONID_(.*?)=(.*?)(?:;|,(?!\s))</td>
  </tr>
  <tr>
    <td>入力</td>
    <td>dicResHeader("Set-Cookie") + ";"</td>
  </tr>
</table>

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3322296/d2c35009-82c1-2640-5487-11dca887f425.png)

抽出結果を変数 ``strSapSessionId`` に格納します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3322296/8fd7606e-87a1-c877-f380-1e46afd43ab0.png)

````vb:代入イメージ
strSapSessionId = regexResult(0).Groups(2).ToString
````

以上でセッション ID と CSRF トークンを利用できるようになりました。

### 購買発注の呼び出し
#### リクエストデータの構築
``/A_PurchaseOrder`` に渡すリクエストデータを用意します。

リクエストデータは JSON 形式で渡します。JSON の作成は Dictionary 型の変数にキーと値を設定していき、最後に ``JsonConvert`` クラスの ``SerializeObject(Dictionary型変数)`` メソッドを使って Dictionary 型から JSON 文字列に変換します。

リクエストデータの構築には下記変数を使います。
<table>
  <thead>
    <tr>
      <th>変数名</th><th>型</th>
    </tr>
  </thead>
  <tr>
    <td>dicPayload</td>
    <td>Dictionary&lt;Of String, Object&gt;</td>
  </tr>
  <tr>
    <td>lstItems</td>
    <td>List&lt;Of Object&gt;</td>
  </tr>
  <tr>
    <td>dicItem</td>
    <td>Dictionary&lt;Of String, String&gt;</td>
  </tr>
  <tr>
    <td>strPayload</td>
    <td>String</td>
  </tr>
</table>

``代入`` または ``複数代入`` アクティビティで購買発注に必要な情報を設定していきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3322296/f5b20e40-cf32-287c-25e5-78d66a633ec2.png)

上記実装を VB.NET のコードで表現すると以下のようになります。

```vb:リクエストデータ構築イメージ
' ヘッダ情報を作成
Dim dicPayload As New Dictionary(Of String, Object)

dicPayload("CompanyCode") = "1710"
dicPayload("PurchaseOrderType") = "NB"
dicPayload("PurchasingGroup") = "001"
dicPayload("PurchasingOrganization") = "1710"
dicPayload("Supplier") = "17300001"

' 明細情報を作成
Dim dicItem As New Dictionary(Of String, String)

dicItem("Material") = "TG11"
dicItem("NetPriceAmount") = "130.00"
dicItem("NetPriceQuantity") = "1"
dicItem("OrderQuantity") = "1"
dicItem("Plant") = "1710"
dicItem("PurchaseOrderItemCategory") = "0"

Dim lstItems As New List(Of Object)
lstItems.Add(dicItem)

' ヘッダに明細を追加
dicPayload("to_PurchaseOrderItem") = lstItems

' JSON 文字列に変換
strPayload = JsonConvert.SerializeObject(dicPayload)
```

実行すると以下の JSON 文字列が ``strPayload`` 変数に格納されます。
```json
{"CompanyCode":"1710","PurchaseOrderType":"NB","PurchasingGroup":"001","PurchasingOrganization":"1710","Supplier":"17300001","to_PurchaseOrderItem":[{"Material":"TG11","NetPriceAmount":"130.00","NetPriceQuantity":"1","OrderQuantity":"1","Plant":"1710","PurchaseOrderItemCategory":"0"}]}
```

#### HTTP 要求アクティビティの構築


続いて ``HTTP 要求`` アクティビティを配置します。以下のプロパティを指定します。

<table>
  <thead>
    <tr>
      <th>プロパティ</th><th>値</th>
    </tr>
  </thead>
  <tr>
    <td>エンドポイント</td>
    <td>strUri</td>
  </tr>
  <tr>
    <td>要求メソッド</td>
    <td>POST</td>
  </tr>
  <tr>
    <td>HTTP ヘッダー</td>
    <td>
      sap-client : strClient<br />
      x-csrf-token : strCsrfToken
    </td>
  </tr>
  <tr>
    <td>Cookie</td>
    <td>
      SAP_SESSIONID_S4H_804 : strSapSessionId<br />
      <br />
      セッション ID 名は ``SAP_SESSIONID_&lt;sid&gt;_&lt;client&gt;`` の形式で、&lt;sid&gt;と&lt;client&gt;の部分は環境により異なります。ご利用の環境に合わせてセッション ID 名を設定してください。
    </td>
  </tr>
  <tr>
    <td>本文形式</td>
    <td>application/json</td>
  </tr>
  <tr>
    <td>本文</td>
    <td>strPayload</td>
  </tr>
  <tr>
    <td>受け入れ形式</td>
    <td>JSON</td>
  </tr>
  <tr>
    <td>応答コンテンツ</td>
    <td>strResContent</td>
  </tr>
  <tr>
    <td>応答ステータス</td>
    <td>intStsCode</td>
  </tr>
</table>

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3322296/45366831-988b-b8d5-98e5-e0bd24dd20e3.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3322296/d61c6dd8-be13-db00-9fec-7b0723693a02.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3322296/a734cdc6-a2e8-7982-0ec0-b0f2b3e8f15f.png)

最後に、HTTP 呼び出し結果から伝票番号を取得します。レスポンスデータは JSON 形式となっているので、``JSON を逆シリアル化`` アクティビティを使って JObject 型に変換します。プロパティは次の通りです。

<table>
  <thead>
    <tr>
      <th>プロパティ</th><th>値</th>
    </tr>
  </thead>
  <tr>
    <td>TypeArgument</td>
    <td>Newtonsoft.Json.Linq.JObject</td>
  </tr>
  <tr>
    <td>JSON 文字列</td>
    <td>strResContent</td>
  </tr>
  <tr>
    <td>Json オブジェクト</td>
    <td>jsonResContent</td>
  </tr>
</table>

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3322296/e95800c3-de51-9578-2a30-5ed8ec30982d.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3322296/18bed060-df51-f7c5-54fe-c83816b8d4c9.png)

JObject の変数 ``jsonResContent`` から伝票番号を取得します。今回はメッセージボックスに結果を表示させてみます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3322296/ba21afd3-30b2-3206-b769-788ddfddffb7.png)

こちらが完成したワークフローです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3322296/729f2b51-15f9-d2d9-1982-d1572d8abc70.png)

## 実行してみる
完成したワークフローを実行してみましょう。発注伝票が登録され、以下のように伝票番号が表示されれば成功です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3322296/ec91351c-1175-f03c-35a7-b7e1efc66d20.png)

SAP GUI でも確認することができました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3322296/018ef621-ca8a-fd0f-cab5-5b4c973c9b10.png)

## おわりに
UiPath から SAP の OData を呼び出すことが出来ました。UiPath では画面の自動化だけでなく、HTTP ベースの API 呼び出しも簡単に実装することが出来ます。
皆様の開発の一助になれば幸いです。

UiPath から SAP S/4HANA の OData を呼び出すシリーズ

https://qiita.com/MasahiroHarada1105/items/f3d92f7d54de40b67f47

https://qiita.com/MasahiroHarada1105/items/83f61fd0a22cd9dee825