SAP GUI Scriptingで自動操縦してみた

自身の業務で、SAPを使う機会があったのですが、そもそもこのSAP、標準でSAP上の操作をExcelのマクロ機能のように録画し、再生するという機能である「SAP GUI Scripting」という機能が備わっています。よって、誰でも簡単に操作を記録して、しかも記録内容はVBSファイルで出力されるので、加工し、Excel VBAなどに組み込んで利用可能であるため、非常に汎用性が高い。

RPAでわざわざSAPの操作を記録してなんてやるよりも、直接VBAからExcelのデータを送り込んだり、エクスポートさせたものを取り込んで整形⇒Teamsに通知なんてことをVBAで仕込めるので、どこぞの超高額RPAなんて要らないのではないかと思います。

今回使用するアプリ

  • SAP GUI

今回は、VBAなどを使って操縦し加工する手前までの単純にクエリの出力までを装備するものになりますので、SAP GUI以外は不要です。ちなみに、公式では昔からRFCにVBSから接続して処理するといったサンプルが出てたりと、割と自動化を結構前から装備できる仕組みが用意されていたりします。

SAP標準機能なので、この機能の為の追加支払いコストはありません。また、実行もWindows標準機能のVBSで動かしてるので、追加セットアップも必要ありません

VBScriptの非推奨化

2023年10月10日、MicrosoftからVBScriptの非推奨化と将来的なWindowsからの機能削除が発表されました。長年に渡って利用されてきたVBScriptですが、これにてDeprecatedが発表されたことで様々な領域への影響が非常に大きいと思われます。VB構文で書けて、情シスでも自動化の1つの選択肢として使ってきたり、デスクトップ自動化でもCOM経由で操縦できるVBScriptは非常に便利でした。

そして、この影響はVBScriptで自動操縦を行ってるSAP GUI Scriptingにも及ぶ為、今後どうなるのか?PowerShellに書き換えが必要ですが、ちょっと普通のユーザで出来ることじゃないので、SAP側が抜本的な変更を迫られることになるかと思います。

※VBAはVBSのようにすぐに無くなるということにならないので、VBSで出力した内容をExcel VBAに移植すればSAPの自動操縦は引き続き可能です。多少の修正で出来るだけでなく、Excelに貼り付けなどの操作はむしろVBAのほうが楽なのでオススメです。

※Microsoft Power Automate for DesktopにてSAPの操縦が装備されましたので、SAP GUI Scriptingよりこちらで実装したほうがパラメータの変更や変数なども利用できるので移行先としてオススメです。SAP自動化というアクションが追加されてるので、こちらでポチポチ構築することになります。

※MicrosoftよりVBScriptの廃止ロードマップの一部が公開。2027年以降はデフォルトで無効化されるようです。それまでにPowerShellに移行するようにしましょう。

図:2027年までに対策をしておきましょう

SAP Automation with Power Automate Desktop (Full Tutorial)

Microsoft Power Automate DesktopでRPAを実現してみる

事前準備

事前準備といってもおそらく標準で使えるようになっているので、その確認作業になります。

  1. SAP GUIを起動する
  2. 左上のハンバーガーメニュー(≡)をクリックして、オプションを開く
  3. 左のメニューの「アクセシビリティ&スクリプト」を開く
  4. さらにスクリプトを開く
  5. 右側のパネルの「スクリプト有効化」にチェックが入ってることを確認します。
  6. スクリプト有効化以外の「通知」などのチェックは全て外します
  7. サイドバーからセキュリティを開く
  8. セキュリティ設定を開いて、無効化します。
  9. チェックしたら、適用⇒OKをクリックして閉じる

図:有効化していればすぐ使えます。

図:セキュリティ設定は無効化します。

使ってみる

操作を記録する

操作の記録をするためには、まずSAP GUIにログインしておきます。ログインが完了したら、以下の手順で操作を記録していきます。Excelのマクロの記録と同じ感覚でVBSファイルが出来上がります。

  1. ツールバーの歯車(⚙)アイコンをクリックする
  2. スクリプト記録と再生という項目があるので、クリックする
  3. 記録と再生という小さなツールボックスが出てくるので、「追加」をクリックする
  4. 保存先の[...]をクリックして、VBSファイルの出力先を指定します(ファイル名であるxxx.vbsまでしっかり指定が必要)。動かない事があるので、その場合は直接パスを入力
  5. 」をクリックすると記録開始。「」をクリックで停止。「」で再生されるようになっています。
  6. 記録開始したら、あとは普通に操作する。但し無駄な動作も全部記録されてしまうので、いつもより慎重に一個ずつ丁寧に操作します。
  7. 記録が完了したら、「」をクリックすれば、VBSファイルが出力されているはずです。

図:録画ツールを出す為のメニュー項目

図:ツールボックスの表示

自動化を実行する

きちんと丁寧に記録したものであれば、SAP GUIにログインした状態で、出力されたxxxx.vbsファイルをダブルクリックするだけで、自動実行が開始されます。非常に高速で実行されるので、RPAよりもはるかに早く処理が終わると思います。

ただし、Excel出力した時に閉じる動作などを記録してしまうと、時々リモートプロシージャコールの失敗でエラーになったりしますが、そこだけ注意すれば、例えばデータを

  • Excel形式ではなくタブ区切りファイルでローカル出力
  • 出力されたものをExcel VBAでインポート処理
  • また、画面下部からは直接xlsx形式での出力オプションも用意されています。

といった形で、Excel形式にする事が可能になると思います。標準ではドキュメントフォルダ\SAP GUI\以下にファイルが保存されるようになっています。自動実行時に例えばBoxのフォルダ内にファイルを作ろうとすると、SAP GUI セキュリティの画面が出てきます。これは自動化記録が出来ないので、もし問題がなければ、事前準備でも使った設定の中にあるセキュリティ通知を無効にすると出てこなくなります。

図:SAP GUIセキュリティ通知を無効にする

図:問題のセキュリティ通知ポップアップ

VBSファイルの中身を見てみる

実際に生成されたvbsファイルの中身を見てみると以下のようなコードになっています。現場で使う上でこれだけでは足りないので、ファイル名や保存先フルパスなどを足してコードを改造してあります。

If Not IsObject(application) Then
   Set SapGuiAuto  = GetObject("SAPGUI")
   Set application = SapGuiAuto.GetScriptingEngine
End If
If Not IsObject(connection) Then
   Set connection = application.Children(0)
End If
If Not IsObject(session) Then
   Set session    = connection.Children(0)
End If
If IsObject(WScript) Then
   WScript.ConnectObject session,     "on"
   WScript.ConnectObject application, "on"
End If

'ログオンユーザ名を取得
Dim logon, username
Set logon = WScript.CreateObject("WScript.Network")
username = logon.UserName

'保存場所のフルパスを生成
Dim fullpath
fullpath = "C:\Users\" & username & "\Box\SAPGUIテスト置き場"

'yyyymmddhhmmss形式の日付日時を取得
Dim strFormattedDate
 
'yyyy/mm/dd hh:mm:ss を yyyymmddhhmmss に変換
strFormattedDate = Replace(Replace(Replace(Now(), "/", ""), ":", ""), " ", "")

'ファイル名を生成
Dim filename
filename = strFormattedDate & "【出向者情報】.xlsx"

'SAPの操作をするメインルーチン
session.findById("wnd[0]").resizeWorkingPane 125,28,false
session.findById("wnd[0]/usr/cntlIMAGE_CONTAINER/shellcont/shell/shellcont[0]/shell").doubleClickNode "F00004"
session.findById("wnd[0]/tbar[1]/btn[6]").press
session.findById("wnd[1]/usr/cmbDYNP4300-DD_USERGROUP").setFocus
session.findById("wnd[1]/usr/cmbDYNP4300-DD_USERGROUP").key = "189"
session.findById("wnd[1]/usr/tblSAPLAQ_INT_FUNCTIONSTCH_OPEN_QUERIES/txtDYNP4300_TC_QUERIES-TEXT[1,9]").setFocus
session.findById("wnd[1]/usr/tblSAPLAQ_INT_FUNCTIONSTCH_OPEN_QUERIES/txtDYNP4300_TC_QUERIES-TEXT[1,9]").caretPosition = 9
session.findById("wnd[1]").sendVKey 2
session.findById("wnd[0]/usr/subSUB_MAIN:SAPLAQ_ADHOC:0210/subSUB_APPLICATION:SAPLHR_QUERY_APPL_AREA:0122/subSUB:SAPLHR_QUERY_APPL_AREA:0121/btnDYNP121-EVALUATION_PERIOD_TEXT").press
session.findById("wnd[0]/usr/subSUB_MAIN:SAPLAQ_ADHOC:0210/subSUB_APPLICATION:SAPLHR_QUERY_APPL_AREA:0122/subSUB:SAPLHR_QUERY_APPL_AREA:0120/cmbDD_DATE").setFocus
session.findById("wnd[0]/usr/subSUB_MAIN:SAPLAQ_ADHOC:0210/subSUB_APPLICATION:SAPLHR_QUERY_APPL_AREA:0122/subSUB:SAPLHR_QUERY_APPL_AREA:0120/cmbDD_DATE").key = "2"
session.findById("wnd[0]/shellcont[0]/shell").pressToolbarButton "GET_DATA"
session.findById("wnd[0]/shellcont[0]/shell").pressToolbarContextButton "&MB_EXPORT"
session.findById("wnd[0]/shellcont[0]/shell").selectContextMenuItem "&XXL"
session.findById("wnd[1]/tbar[0]/btn[0]").press
session.findById("wnd[1]/usr/ctxtDY_PATH").text = fullpath
session.findById("wnd[1]/usr/ctxtDY_FILENAME").text = filename
session.findById("wnd[1]/usr/ctxtDY_FILENAME").caretPosition = 12
session.findById("wnd[1]/tbar[0]/btn[0]").press
session.findById("wnd[0]/sbar").doubleClick
session.findById("wnd[0]/tbar[0]/btn[12]").press
  • findByIdがSAP GUIの各パーツのSAPIDを探してアクションをしてるものになります。
  • 見た目はなんとなく、UIAutomationに似ていますが、SAP GUI操作をする独自のものになります。
  • Excel形式での保存先、またその時のファイル名はyyyyMMddhhmmss形式のファイル名としてBoxの所定のフォルダに保存するよう改造してあります。
  • Boxの場合ログオンユーザ名毎に所定のフォルダが変わるので、ログオンユーザ名を取得して動的にフルパスを生成しています。

他のアドホッククエリなどで日付の指定などを外部からパラメータで送りたい場合には、VBSにてインプットボックスを表示して入れてもらうか?また、Excel VBAなどから直接vbsファイルを呼び出す時に、引数でVBAに渡して利用するなどの工夫が必要になります。

特定のクエリを確実に実行する方法

特定のユーザグループにアドホッククエリを追加していくと、当然ながら順番などがズレます。しかし、SAP GUI Scriptingで生成されるVBSでこれらのクエリの実行は、特定の名称でヒットさせているのではなく、単純に何番目のクエリという形で実行させているので、このままでは「クエリを追加したり削除した場合」に実行対象のクエリの位置がズレてしまうことになります。

そこで、この解決法を探した所、SAPコミュニティに同様の事例の投稿があり、そちらのコードを修正して使うことにしました。

'グリッドコントロールを取得
Set GridView = session.findById("wnd[1]/usr/tblSAPLAQ_INT_FUNCTIONSTCH_OPEN_QUERIES/")

Dim counter : counter = 0
Dim titleman : titleman = "ここにクエリの名称を入れる"  
Dim cellval

For i = 0 To GridView.RowCount - 1
	'1個スクロールさせる
	session.findById("wnd[1]/usr/tblSAPLAQ_INT_FUNCTIONSTCH_OPEN_QUERIES").verticalScrollbar.position = i
	
	'名称列の値を取得
	cellval = session.findById("wnd[1]/usr/tblSAPLAQ_INT_FUNCTIONSTCH_OPEN_QUERIES").GetCell(0, 0).Text

	'目的のタイトルと一致するか?
	if cellval = titleman Then
		exit for
	end if

	'カウントアップ
	counter = counter + 1
Next

'出力するクエリを選択して表示
session.findById("wnd[1]/usr/tblSAPLAQ_INT_FUNCTIONSTCH_OPEN_QUERIES/txtDYNP4300_TC_QUERIES-TEXT[1,0]").setFocus
session.findById("wnd[1]/usr/tblSAPLAQ_INT_FUNCTIONSTCH_OPEN_QUERIES").getAbsoluteRow(counter).Selected = True
session.findById("wnd[1]/tbar[0]/btn[0]").press
  • クエリ一覧のダイアログが出てから、クエリ選択・実行までのコードです
  • クエリの名称と一致するものを1個ずつグリッドに表示されてる名称と一致するか?検証しては1個スクロールさせています。
  • 一致したらループを抜ける。そのため、検証中にcounterを回しておきます。
  • クエリの選択はgetAbsoluteRowにてcounterの値を指定し、Selectedで選択させることが可能です。
  • ボタンをクリックさせて、クエリが実行されます。
  • wnd[1]/usr/tblSAPLAQ_INT_FUNCTIONSTCH_OPEN_QUERIES」の部分は、SAP GUI Scriptingで生成させたときに出てくるので、それを利用します。環境によって変わるので注意。

このグリッドがちょっと変わった仕様で、1度に表示できるのが10個まで、次の10個はスクロールさせないと値が取れない仕組み。10個ずつでスクロールさせてもうまく取得が出来なかったのですが、1つとって1スクロールだとクエリ名が問題なく取得が出来たので、常にスクロールさせた一番上のクエリを取る仕組みになっています。

図:名称の部分でヒットさせて行番号を取得するのがポイント

Excel VBAから呼び出す

ソースコードの移植

元々のコードがVBSである為、VBAに移植するのは実は非常に簡単です。Public Functionを用意して、基本的にはコピペ。必要な変数の宣言を追加するくらいで動いてしまいます(参照設定等は必要ありません)

以下はVBAの場合のコードになります。

Public Function autoSAP_stream()
    '各種変数の宣言
    Dim SapGuiAuto As Object
    Dim Application As Object
    Dim Connection As Object
    Dim session As Object
 
    Set SapGuiAuto = GetObject("SAPGUI")
    Set Application = SapGuiAuto.GetScriptingEngine
    Set Connection = Application.Children(0)
    Set session = Connection.Children(0)
  
    '以下は不要なのでコメントアウト(削除して問題ない)
    '   WScript.ConnectObject session, "on"
    '   WScript.ConnectObject Application, "on"
    
    'ログオンユーザ名を取得
    Dim logon, username
    Set logon = CreateObject("WScript.Network")
    username = logon.username
    
    '保存場所のフルパスを生成
    Dim fullpath
    fullpath = "C:\Users\" & username & "\Box\SAPGUIテスト置き場"
    
    'yyyymmddhhmmss形式の日付日時を取得
    Dim strFormattedDate
     
    'yyyy/mm/dd hh:mm:ss を yyyymmddhhmmss に変換
    strFormattedDate = Replace(Replace(Replace(Now(), "/", ""), ":", ""), " ", "")
    
    'ファイル名を生成
    Dim filename
    filename = strFormattedDate & "【出向者情報】.xlsx"
    
    'VBSからコピペ
    'SAPの操作をするメインルーチン
    session.findById("wnd[0]").resizeWorkingPane 125, 28, False
    session.findById("wnd[0]/usr/cntlIMAGE_CONTAINER/shellcont/shell/shellcont[0]/shell").doubleClickNode "F00004"
    session.findById("wnd[0]/tbar[1]/btn[6]").press
    session.findById("wnd[1]/usr/cmbDYNP4300-DD_USERGROUP").SetFocus
    session.findById("wnd[1]/usr/cmbDYNP4300-DD_USERGROUP").Key = "189"
    session.findById("wnd[1]/usr/tblSAPLAQ_INT_FUNCTIONSTCH_OPEN_QUERIES/txtDYNP4300_TC_QUERIES-TEXT[1,9]").SetFocus
    session.findById("wnd[1]/usr/tblSAPLAQ_INT_FUNCTIONSTCH_OPEN_QUERIES/txtDYNP4300_TC_QUERIES-TEXT[1,9]").caretPosition = 9
    session.findById("wnd[1]").sendVKey 2
    session.findById("wnd[0]/usr/subSUB_MAIN:SAPLAQ_ADHOC:0210/subSUB_APPLICATION:SAPLHR_QUERY_APPL_AREA:0122/subSUB:SAPLHR_QUERY_APPL_AREA:0121/btnDYNP121-EVALUATION_PERIOD_TEXT").press
    session.findById("wnd[0]/usr/subSUB_MAIN:SAPLAQ_ADHOC:0210/subSUB_APPLICATION:SAPLHR_QUERY_APPL_AREA:0122/subSUB:SAPLHR_QUERY_APPL_AREA:0120/cmbDD_DATE").SetFocus
    session.findById("wnd[0]/usr/subSUB_MAIN:SAPLAQ_ADHOC:0210/subSUB_APPLICATION:SAPLHR_QUERY_APPL_AREA:0122/subSUB:SAPLHR_QUERY_APPL_AREA:0120/cmbDD_DATE").Key = "2"
    session.findById("wnd[0]/shellcont[0]/shell").pressToolbarButton "GET_DATA"
    session.findById("wnd[0]/shellcont[0]/shell").pressToolbarContextButton "&MB_EXPORT"
    session.findById("wnd[0]/shellcont[0]/shell").selectContextMenuItem "&XXL"
    session.findById("wnd[1]/tbar[0]/btn[0]").press
    session.findById("wnd[1]/usr/ctxtDY_PATH").Text = fullpath
    session.findById("wnd[1]/usr/ctxtDY_FILENAME").Text = filename
    session.findById("wnd[1]/usr/ctxtDY_FILENAME").caretPosition = 12
    session.findById("wnd[1]/tbar[0]/btn[0]").press
    session.findById("wnd[0]/sbar").DoubleClick
    session.findById("wnd[0]/tbar[0]/btn[12]").press

End Function
  • WScript.ConnectObjectの部分は必要無いのでコメントアウト
  • 冒頭にVBSでは無かった変数の宣言を追加します。
  • ログオンユーザの取得は、CreateObject("WScript.Network")に変更してるだけ。
  • あとはVBSコードをそのままコピペしてる

よって、Excel VBAから実行させる場合には、VBAからシート内のデータにアクセスしてそれをコードに簡単に渡せるので、VBSよりも更に利便性がアップします。また、設定関係のGUIなどを別途フォームで用意も出来たりするので、iniファイルに予め保存しておいたり(例えば、出力先など)をコードを弄らず変更可能に出来たりするので、オススメです。SAP GUI Scripting単体ではSAPしか操作出来ませんが、VBAからならば、色々自動化に繋げられるので、以下のエントリーも参考にしましょう。

VBAで他のアプリケーションを操作する

SAP GUIの起動~ログインまで自動化

SAP GUI Scriptingはログイン自体の自動化もできるようになっています。しかし、ログイン前の接続先の選択等でログオンボタンを押す部分には手が出せないので、ここをVBAのSendkeyを使って処理をすることで自動起動⇒自動接続⇒自動ログオンが実現できます。パスワードの保全だけ何か安全な手法で暗号化復号化が必要にはなると思います。

また、この時スクリプトの実行時にメッセージが出るのが鬱陶しいので、スクリプト通知はオプション設定からオフにしておきましょう。

#If VBA7 Then
Private Declare PtrSafe Sub Sleep Lib "kernel32" (ByVal ms As LongPtr)
#Else
Private Declare Sub Sleep Lib "kernel32" (ByVal ms As Long)
#End If

Public Function autoSAP_stream()
    'SAP GUIを起動してEnterキーを送る
    Dim sappath As String
    sappath = "C:\Program Files (x86)\SAP\FrontEnd\SAPgui\saplogon.exe"

    Dim rc As Long
    rc = Shell(sappath, vbNormalFocus)
    Sleep 5000
    
    'WSH用の変数の宣言
    Dim wshshell As Object
    Set wshshell = CreateObject("WScript.Shell")
    wshshell.SendKeys ("{ENTER}")

    ・・・・中略・・・・

    '自動ログイン
    session.findById("wnd[0]").resizeWorkingPane 125, 28, False
    session.findById("wnd[0]/usr/txtRSYST-BNAME").Text = "ユーザIDをここに入れる"
    session.findById("wnd[0]/usr/pwdRSYST-BCODE").Text = "パスワードをここに入れる"
    session.findById("wnd[0]/usr/pwdRSYST-BCODE").SetFocus
    session.findById("wnd[0]/usr/pwdRSYST-BCODE").caretPosition = 11
    session.findById("wnd[0]").sendVKey 0



End Function
  • 自分の場合、接続先が一番上にあるので、Enterキーを送るだけでログオン画面に行けるので、Enterキーだけ送っています。異なる場合は、TABや方向キーなどを送りつけて、接続先を選択するようにすると良いでしょう。
  • 起動すぐにキーを送ると、まだ起動していないのにキーが送信されてしまうので、Sleepで5秒間ウェイトを掛けています
  • 省略以下は前述の処理と同じ。通知メッセージを切ってるので、自動処理が完了するまであとは一直線です。
  • ユーザIDとパスワードを直書きでVBAコード内に入れるのはやめましょう。暗号化・復号化を別に装備したり、資格情報マネージャから取り出すなどの装備が別途必要です。

図:スクリプト実行時の通知はオフにしておく

資格情報マネージャ読み書きプログラムをNode.jsで作成する

Electronから呼び出す

現在、これら作成したVBSファイルを管理(渡すパラメータや時限発火させる日時など)する為のプログラムを作成中ですが、以前も紹介したテクニックを応用して、Electron + Node-Cronを利用して指定時刻に自動実行するように構築しています(既にプログラムは概ね完成)。

今回のVBSは引数を取得してSAPを自動操縦するようにしている為、これらをうまく同期的に処理させないとマズイので、工夫が必要です。

VBSと連携するElectronアプリを作る

図:管理用プログラムがあれば尚便利になる

Node.js側コード

//タスクを手動実行
ipcMain.on('exectask', function (event, ...args) {
    //引数を分解
    var temp = JSON.parse(args[0]);

    //引数を取得
    var vbsfile = temp.args;
    var taskname = temp.args2;
    var exportman = temp.args3;
    var filename = temp.args4;

    //メッセージオプション
    var options = {
        type: 'info',
        title: "実行確認",
        buttons: ['Yes', 'No'],
        message: 'タスクの手動実行',
        detail: taskname + "タスクを手動実行しますか?"
    }

    //表示する
    var ret = dialog.showMessageBoxSync(mainWindow, options);
    if (ret == 0) {
        //処理を続行する
    } else {
        //処理を中断する
        event.sender.send('message', "実行処理はキャンセルされました。");
        return;
    }

    //コマンドラインを構築
    sapcheck(function(ret){
        if(ret == 1){
            //起動エラー
            //エラーが発生しているので、なにか処理をする
            event.sender.send('message', "SAP起動エラー");
            return;
        }else{
            //コマンドライン引数を構築する
            var cmdargs = exportman + "," + filename;

            //VBS実行
            var ret = vbsCommand(vbsfile,cmdargs);

            //返り値により処理を分岐(1でエラー、3で成功)
            if(ret == 1){
                //エラーが発生しているので、なにか処理をする
                event.sender.send('message', "実行エラー");
                return;
            }else{
                //メッセージを表示
                event.sender.send('closeman');
            }
        }
    })    
});

//SAP起動チェックコマンド
function sapcheck(callback) {
    //SAPチェック用コマンド
    var vbsfile = __dirname + '/vbs/sapcheck.vbs'

    //引数を構築
    //ログインパスワードを取得
    if(store.get("sapid") != "undefined" || store.get("sapid") != null){
        var servicename = servicebase + store.get("sapid");

        if(keytar.findPassword(servicename)){
            const secret = keytar.getPassword(servicename,store.get("sapid"));
            secret.then((result) => {
                //引数を構築
                var cmdargs = store.get("sapid") + "," + result;

                //コマンドを組み立てて実行
                var child = spawnSync('cscript.exe', [ vbsfile, cmdargs ] );
            
                //返り値を取得する(status)
                var ret = child.status;
            
                //retを返す
                callback(ret);
            });
        }else{
            //エラーを返す
            callback(1);
            return;
        }
    }else{
        //エラーを返す
        callback(1);
        return;
    }
}

//外部コマンドを実行する
function vbsCommand(fullpath,args) {

    //コマンドを組み立てて実行
    var child = spawnSync('cscript.exe', [ fullpath, args ] );
   
    //返り値を取得する(status)
    var ret = child.status;

    //retを返す
    return ret;
}
  • SAPのパスワードは資格情報マネージャに格納しているので、keytarを利用して読み書きを行っています。
  • 呼び出し前にsapcheck関数で、SAPが起動してるかどうかを確認し、していなければ起動させてログオンしてから次の処理に渡しています。
  • 引数はカンマ区切りでcmdargsに格納し、vbsのファイルへと渡しています。
  • VBS側からのstatusの返り値を取得してから次の処理へと移っています。

VBS側コード

自動起動させるVBS
'引数を取得する
Dim args : args = WScript.Arguments(0)
Dim aryStrings
aryStrings = Split(args, ",")

'引数を分解する
Dim loginid : loginid = aryStrings(0)
Dim passwd : passwd = aryStrings(1)

'SAP GUIを起動してEnterキーを送る
Dim sappath
sappath = """C:\Program Files (x86)\SAP\FrontEnd\SAPgui\saplogon.exe"""

'Shellを起動する
Dim oWshShell
Set oWshShell = CreateObject("WScript.Shell")
dim status : status = 1

'エラー発生時に処理を継続
On Error Resume Next

'SAP起動中確認
Dim item, items, exeman, flgman, bootman
flgman = 0  'SAP起動フラグ
 
'起動プロセス一覧をループで調査
Set items = CreateObject("WbemScripting.SWbemLocator").ConnectServer.ExecQuery("Select * From Win32_Process")
For Each item In items
	'EXE名を取得
	exeman = item.Description
	
	'saplogon.exeの場合はフラグを立てる
	if exeman = "saplogon.exe" Then
		flgman = 1
	end if
Next

'SAPを起動する
if flgman = 0 Then
	'SAP GUIを起動する
	oWshShell.Run sappath
	WScript.Sleep 5000

	'Windowをアクティブにする
	Do
		If oWshShell.AppActivate("SAP") Then Exit Do
		WScript.Sleep 100
	Loop

	'WSH用の変数の宣言
	oWshShell.SendKeys("{ENTER}")
	WScript.Sleep 5000
end if

'SAPに接続する
If Not IsObject(application) Then
   Set SapGuiAuto  = GetObject("SAPGUI")
   Set application = SapGuiAuto.GetScriptingEngine
End If
If Not IsObject(connection) Then
   Set connection = application.Children(0)
End If
If Not IsObject(session) Then
   Set session    = connection.Children(0)
End If
If IsObject(WScript) Then
   WScript.ConnectObject session,     "on"
   WScript.ConnectObject application, "on"
End If

'ログイン処理
'自動ログイン
if flgman = 0 Then
	session.findById("wnd[0]").resizeWorkingPane 125, 28, False
	session.findById("wnd[0]/usr/txtRSYST-BNAME").Text = loginid
	session.findById("wnd[0]/usr/pwdRSYST-BCODE").Text = passwd
	session.findById("wnd[0]/usr/pwdRSYST-BCODE").SetFocus
	session.findById("wnd[0]/usr/pwdRSYST-BCODE").caretPosition = 11
	session.findById("wnd[0]").sendVKey 0
	WScript.Sleep 5000
end if

'エラーが発生しているかをチェックします。
If Err.Number <> 0 Then
	status = 1
	Set oWshShell = Nothing
Else
	'無事完了したのでステータスを0にする
	status = 3
	Set oWshShell = Nothing
End If

'エラー時ジャンプ先
On Error Goto 0

'ステータスを返す
WScript.Quit status
  • 引数はIDとPWをカンマ区切りで受け取り、VBS側で分解して利用しています。
  • SAPを操作できるのはログオン画面以降からなので、最初のログオンボタンを押す部分については、ウィンドウをアクティブにしてからSendkeysでキーを送って次に進んでいます
  • saplogon.exeが起動してるかしていないかで、起動チェックを掛けています。
  • 無事に起動済みになったら、statusは3を返します。起動失敗時には1を返します。
メインのSAP操作用VBS

最後に勝手に起動するExcelを終了させる為のコマンドはこちらのサイトからお借りしています。

'変数を宣言し、引数(メアド)を取得する
dim status : status = 1
Dim args : args = WScript.Arguments(0)
Dim aryStrings
aryStrings = Split(args, ",")

'引数を分解する
Dim fullpath : fullpath = aryStrings(0)
Dim filename : filename = aryStrings(1)

'エラー発生時に処理を継続
On Error Resume Next

'SAP GUIと接続する
If Not IsObject(application) Then
   Set SapGuiAuto  = GetObject("SAPGUI")
   Set application = SapGuiAuto.GetScriptingEngine
End If
If Not IsObject(connection) Then
   Set connection = application.Children(0)
End If
If Not IsObject(session) Then
   Set session    = connection.Children(0)
End If
If IsObject(WScript) Then
   WScript.ConnectObject session,     "on"
   WScript.ConnectObject application, "on"
End If

'ログオンユーザ名を取得
Dim logon, username
Set logon = WScript.CreateObject("WScript.Network")
username = logon.UserName
 
'yyyymmddhhmmss形式の日付日時を取得
Dim strFormattedDate
 
'yyyy/mm/dd hh:mm:ss を yyyymmddhhmmss に変換
strFormattedDate = Replace(Replace(Replace(Now(), "/", ""), ":", ""), " ", "")
 
'ファイル名を生成
Dim fileman
fileman = strFormattedDate & filename & ".xlsx"

'SAP処理メインルーチン
session.findById("wnd[0]").resizeWorkingPane 125, 28, False
session.findById("wnd[0]/usr/cntlIMAGE_CONTAINER/shellcont/shell/shellcont[0]/shell").doubleClickNode "F00004"
session.findById("wnd[0]/tbar[1]/btn[6]").press
session.findById("wnd[1]/usr/cmbDYNP4300-DD_USERGROUP").SetFocus
session.findById("wnd[1]/usr/cmbDYNP4300-DD_USERGROUP").Key = "189"
session.findById("wnd[1]/usr/tblSAPLAQ_INT_FUNCTIONSTCH_OPEN_QUERIES/txtDYNP4300_TC_QUERIES-TEXT[1,9]").SetFocus
session.findById("wnd[1]/usr/tblSAPLAQ_INT_FUNCTIONSTCH_OPEN_QUERIES/txtDYNP4300_TC_QUERIES-TEXT[1,9]").caretPosition = 9
session.findById("wnd[1]").sendVKey 2
session.findById("wnd[0]/usr/subSUB_MAIN:SAPLAQ_ADHOC:0210/subSUB_APPLICATION:SAPLHR_QUERY_APPL_AREA:0122/subSUB:SAPLHR_QUERY_APPL_AREA:0121/btnDYNP121-EVALUATION_PERIOD_TEXT").press
session.findById("wnd[0]/usr/subSUB_MAIN:SAPLAQ_ADHOC:0210/subSUB_APPLICATION:SAPLHR_QUERY_APPL_AREA:0122/subSUB:SAPLHR_QUERY_APPL_AREA:0120/cmbDD_DATE").SetFocus
session.findById("wnd[0]/usr/subSUB_MAIN:SAPLAQ_ADHOC:0210/subSUB_APPLICATION:SAPLHR_QUERY_APPL_AREA:0122/subSUB:SAPLHR_QUERY_APPL_AREA:0120/cmbDD_DATE").Key = "2"
session.findById("wnd[0]/shellcont[0]/shell").pressToolbarButton "GET_DATA"
session.findById("wnd[0]/shellcont[0]/shell").pressToolbarContextButton "&MB_EXPORT"
session.findById("wnd[0]/shellcont[0]/shell").selectContextMenuItem "&XXL"
session.findById("wnd[1]/tbar[0]/btn[0]").press

session.findById("wnd[1]/usr/ctxtDY_PATH").Text = fullpath
session.findById("wnd[1]/usr/ctxtDY_FILENAME").Text = fileman
session.findById("wnd[1]/usr/ctxtDY_FILENAME").caretPosition = 12
session.findById("wnd[1]/tbar[0]/btn[0]").press
session.findById("wnd[0]/sbar").DoubleClick
session.findById("wnd[0]/tbar[0]/btn[12]").press

'Excelが起動するので閉じる
WScript.Sleep 5000
TerminateProcess("EXCEL.EXE")

'無事完了したのでステータスを3にする
status = 3

'エラー時ジャンプ先
On Error Goto 0

'ステータスを返す
WScript.Echo status
WScript.Quit status

'プロセスキル
Sub TerminateProcess(ProcessName)
    Dim Service,QfeSet,Qfe
    Set Service = CreateObject("WbemScripting.SWbemLocator").ConnectServer
    Set QfeSet = Service.ExecQuery("Select * From Win32_Process Where Caption='" & ProcessName & "'")
    For Each Qfe In QfeSet
        Qfe.Terminate
    Next
End Sub
  • VBSで処理に必要な引数は1個の固まりとして受け取っています(カンマ区切りで取得)。コレを分解して利用します。
  • 保存先フォルダはログインユーザ名毎に違うので動的にフルパスを生成しています。
  • 処理が成功したらstatusは3を返し、Node.js側で次の処理を実行します。エラーの場合1を返し、処理を停止します。
  • 最後に自動で開かれるExcelファイルをTerminateProcessにてすべて閉じてしまいます。

TerminateProcessの場合だと、他に作業していたExcelまで含めて全て閉じられてしまうのはExcelは1プロセスで複数ブックを処理してる関係でプロセスが独立していないのが、原因。この問題の対応策として、ExcelのプロセスをKillするのではなく、開いてるブックを調べてブックを閉じるだけの処理にすること(その為、空のExcelのプロセスは残る)

その場合は、以下の関数をVBSに追加して、TerminateProcess("Excel.exe")をTerminateWorkbook(filename)に置き換えることで実現可能です。

'ワークブックキル
Sub TerminateWorkbook(filename)
   Dim bk, c, ex
   Set ex = GetObject(, "Excel.Application")

   For Each bk In ex.Workbooks
      If bk.Name = filename Then
         bk.Close
      End If
   Next
End Sub
  • 既存のExcelプロセスが起動していないと、GetObjectでエラーになるので注意
  • bk.closeしてるだけなのでプロセスは残ります
  • ファイル名が一致する場合だけ閉じます(Excelは仕様上、違う場所にある同じファイル名のファイルでも起動は出来ない為)
テーブル化する

前述までのコードの場合、単純に出力したExcelファイルを保存しているだけですが、以下のコードを追加する事で範囲をテーブル化する事が可能です。TerminateProcess("EXCEL.EXE")の後に追加します。

'範囲をテーブル化するルーチン
Dim ex, wb, address

'Excelに接続
Set ex = CreateObject("Excel.Application")
Set wb = ex.Workbooks.Open(fullpath & "\" & filename)

'データ範囲を取得
address = wb.Sheets("Sheet1").UsedRange

'データ範囲をテーブル化する
wb.Sheets("Sheet1").ListObjects.Add().Name = "tomato"

'保存する
wb.Save

'終了処理
wb.Close
ex.Quit
Set ex = Nothing
Set wb = Nothing

以上のコードでVBSからExcelブックを操作しテーブル化していますが、もしここからコピペをしているようなケースがある場合には

  • ファイル名を固定化し、コピペではなく貼付け先のExcelファイルから今回出力したExcelファイルのテーブルを参照するようにする
  • テーブルを参照は、Power Queryを利用して実現することが可能です。
  • 出力ファイル名を固定化するので、事前に同じファイル名のファイルがある場合には削除する、存在確認をするコードを更に追加すると良いでしょう

Excelで身に付けるべきスキルコース(松)

Power Automate Desktopから操縦する

SAP GUI Scriptingは、SAP上の操作に関しては一部を除いてほぼ完璧に再現出来ます。しかし、以下のような限界点があります。

  1. 固定的なパラメータによる自動化は完璧でも、動的なパラメータによる自動化は管理プログラムのようなものが必要になる(アドホッククエリのパラメータを実行時毎に異なるものにしたい等)
  2. ログオン画面から先は自動化できるが、ログオン自体の自動化は出来ない
  3. Excelデータを元にループで回してSAPを操作するには、結局VBSのコードを大幅に改造する必要がある
  4. SAP操作後の処理については管轄外であるので、本当にSAPの操作のみしか自動化出来ない
  5. 時限発火させる為には、バッチファイルとVBSをタスクスケジューラ等を利用して登録する作業が必要になる

故に、自分は管理プログラムを作ってるわけなのですが、単純な操作の自動化だけを実現しても正直あまり時間短縮の効果はありません。ということで、管理する部分をPower Automate Desktopで作ったらどうなるか?試してみました。

Power Automate Desktopの問題点

SAP GUI Scriptingを使わずに全部をPower Automate Desktopの「デスクトップレコーダ」を使って記録してしまえばいいじゃないか?という人がいるでしょう。しかし実際に作ってみればわかりますが、とりわけSAP上の操作では

  • ダブルクリックの認識がシングルクリックで記録されていたりする
  • 画面UI要素の認識が非常にモッサリしていて、記録が取りこぼされているケースも多々ある
  • ウェイトを意識的に入れないと、要素の操作に失敗する事も結構ある
  • とある要素に至っては赤枠が出ず認識しないケースもあった(Excelでのエクスポートをするボタン等)
  • デスクトップレコーダ自体が相当重いので事務用PCではキツイケースがある(マウスカーソルかくかく)

正直、正確さとスピードに関してはPADで記録するより、SAP GUI Scriptingで記録したほうが、ストレスも無くいちいち認識待ちなど必要無いので、Power Automate Desktop自体でデスクトップレコーダを使ったUI操作はオススメしません。

PADが担当する部分は操作のアシスト

前述の通り、PADが担当するのはSAP GUI Scriptingでは実現出来ない部分の自動化を構築するために利用します。例えば

  • SAP GUIに自動ログオンする
  • SAP GUI Scriptingで用意したVBSの「出力ファイル名」であったり、「パラメータ」部分をPADで都度変更して渡して実行する
  • 繰り返し登録するような「ループ処理」で同じ処理を登録したVBSに何回も作業を投げて回す
  • SAPでファイル出力後以降の、そのファイルに対しての操作を継続して自動化する部分

実際に作ってみる

自動ログイン⇒VBS実行⇒結果を取得までの流れを作っています。ただしいくつかの注意点があります。

  • 冒頭のSAPへの自動ログインはデスクトップレコーダでログインボタンを押す場所までを作成します。
  • ログイン画面以降のIDおよびPWの入力も今回はデスクトップレコーダで作成しています。パスワード類は別の安全に取得できる手段を構築する必要があります。
  • UI要素のRSYST-BNAMEがログインID、RSYST-BCODEがパスワードの入力欄になります。
  • PADのVBScriptを実行アクションは簡単なVBSしか実行出来ないので、今回は前述のElectronで呼び出す場合のVBSファイルを別途用意しておきます。
  • VBSファイルへのフルパス(vbsfile)、出力するファイル名(filename)、出力先フォルダのフルパス(exportpath)をそれぞれPAD上で変数で設定しておきます。
  • コマンドラインを変数の中で構築します。今回は
    cscript //nologo %vbsfile% "%exportpath%,%filename%"

    といった形で構築しています。//nologオプションを入れておかないと、VBSの出力結果(返り値)にcscriptのバージョン情報やcopyrightが入ってしまうので必ず入れておきます。

  • VBSからは無事操作が完了したら、3が返ってくるように一番最後に「WScript.StdOut.WriteLine status」で出力するようにVBS側を修正しています。
  • DOSコマンドを実行アクションで、構築したコマンドラインを実行します。
  • 返り値はCommandOutputで受け取り、メッセージボックスで表示するようにしています。ここで条件分岐やループの次の処理などを組み込んでいくと良いでしょう。

一番のポイントは、標準のVBScriptを実行アクションが使えない点。故にDOSコマンドにてcscript.exeを叩く点です。SAPの起動チェック等も前述のVBSを叩いて処理しても良いでしょう。

図:こんな感じのフローになります。

考察

メンテナンス面での優位性

日本では、本場ドイツやグローバル企業とは異なり、とかく複雑なアドオンを鬼のように開発してSAPに組み込み魔改造する傾向があるというお話を伺ったことがあります。しかも人事給与計算だけでなく他の領域でもかなりの費用と時間を掛けて。

しかし、SAPでも最近ありましたが大型メジャーアップデート時にこれらのアドオンが一斉に動かなくなる可能性もあります。また、平時であってもこれらのメンテナンスコスト(内製であれ外注であれ)馬鹿にならない費用が固定費として経営にのしかかって来ることになります。

これらのうち、わざわざアドオンで作らなくてもいいでしょ?という類のものは、こうしたツールで外側で作っておくほうが、バージョンアップ後でも最低限の修正で済む(SAPと直接連携してるわけじゃなくただ操作を再現してるだけなので)ので、コスト面では圧倒的に優位になります。

自動化の優位性

VBSで出力されるという事は、以下のメリットがあります

  • VBAと殆ど同じ文法なので、誰でも簡単に改造する事が可能になる
  • VBSで動かしてるので、非常に高速に動作する上に事前セットアップなどが必要ない(標準でWindowsでは使える為)
  • VBSというWindows標準機能で自動化を実現しているので、特定RPA製品にベンダーロックインされたり、製品が消えて困ったという事がなくなる
  • 当然、外部RPAを使わず実現できるので、ライセンス費用や専用マシンなんてものは必要なくなる
  • VBSなので、Power Automate Desktopから叩くという形で、Windows標準のRPAをあえて使うという選択も可能になる
  • VBSなので色々な知見やソースコードのサンプルが沢山公開されており、学習コストを低くすることができる。
  • Excel VBAやコマンドライン、さらにはNode.jsなどから引数を渡して叩く事ができるので、本体側で自動化の装備をする必要がない。
  • VBSはVBA同様、Outlookなどのコンポーネントと連携できるので、かなり簡単に上記のコードにメール送信などが追加できる。
  • 操作の記録自体はとっても簡単なので、誰でも自動化スクリプトを生成できる(複雑なものはVBS編集が必要だけれど)
  • マウス操作やキー送信でSAP GUIを操作していないので、ユーザが自動操縦中になんらかの操作をしても影響を受けない。
  • SAP GUI Scripting自体結構歴史があって、利用されてきた実績がある(2010年頃のSAP 6.20から使えているようです)。

関連リンク

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

日本語が含まれない投稿は無視されますのでご注意ください。(スパム対策)