SmartHRの電子申請をGASで最適化した話 After

以下、前回の記事です。

[SmartHRの電子申請をGASで最適化した話](https://qiita.com/shikumiya_hata/items/a5b280896dd8632b3a86)

この記事の後日談的な位置付けで本記事を書いていきます。

# 課題

SmartHRの電子申請にかかる以下の業務を自動化する。

- 添付ファイルのダウンロード
- 添付ファイルZip解凍
- 解凍したファイルをGoogle Driveへのアップロード
- 申請ステータスの変更(アーカイブ)
- Slack通知 or メール送信

## 課題の詳細（再掲）

クラウド人事労務ソフトのSmartHRでは電子申請が行えます。
SmartHRからの電子申請には電子証明書を取得して電子申請を行えるようにする必要があります。

電子申請された手続は、SmartHR上で進捗管理ができ、完了した手続に関しては
SmartHRの画面上から公文書のリンク経由でファイルを直接ダウンロードすることができます。

この機能について申請一つ一つを見ればとても便利なのですが、1ヶ月の間に大量の申請を行うような入退社が多い大企業や、複数の企業を取り扱う社労士、経営管理コンサルタントにとっては不便な点もあります。

公文書の添付ファイルはe-Govで手続を行った時と同様に圧縮されたzipファイルで、SmartHR上から検索することはできません。
従って、そのファイルに後日アクセスすることを考えると、SmartHR外にファイルを保存しておく必要があります。

外部にファイルを保存する場合、手続きごとに画面を切り替えてファイルをダウンロードし、そのファイルを別途社内で用意したストレージに格納することが必要です。この処理を実際に手順単位で書き下ろすと以下の通りです。

【業務手順】 ※5以降は前回から更新があります
1. 顧客毎に用意されたSmartHRの専用URLからログイン
2. 申請の一覧を参照し、特定のステータスのデータを検索
3. 公文書やお知らせといった添付ファイル(zip)をダウンロード
4. ダウンロード・解凍したファイルをGoogle Drive上にアップロード
5. XML/XSLはMacでも読みやすいようにPDFへ変換
6. 解凍/変換されたPDFをリネームして保管
7. 申請データをアーカイブ
8. スプレッドシートのステータスを変更
9. 処理が完了したことをSlack or メールで通知する

とある社労士事務所様では、この作業に50時間/月もの時間を要していると伺いました。
これを自動化してしまえば相当な作業効率化につながります。

上記業務手順をGASで自動化しようというのが今回の課題です。

# 関連技術

- GoogleAppsScript
- [cheerio(※1)](https://www.npmjs.com/package/cheerio)
- Google Spreadsheet
- Google Drive
- Heroku
- Python 3.x / Flask
- [lxml(※2)](https://pypi.org/project/lxml/)
- [zipfile(※3)](https://docs.python.org/ja/3/library/zipfile.html)
- [getpass(※3)](https://docs.python.org/ja/3/library/getpass.html)

※1...cheerioはnpmのライブラリ、GASライブラリ化します
※2...Pythonのライブラリ、pipでインストールします
※3...Python3.xに同梱のライブラリです

# GASの限界と代替手段

課題を解く上で問題が生じました。

GASで以下の2つの処理を実装しようと試みました。

1. MS932の文字コードを含むzip解凍(Windows環境依存)
[[GAS]Utilities.unzip()で発生するエラーの原因と解決策](https://qiita.com/shikumiya_hata/items/8358cbb28312331cd9be)

2. XSLT(XML/XSLをHTMLへ変換)
[GASでXSLTしてみた（XML/XSL→HTML→PDF）](https://qiita.com/shikumiya_hata/items/70d028132ee3b17d1ff4)

大体のnpmライブラリを試行錯誤しましたが、現状のnpmライブラリを使った手法では、全てのパターンへの対応ができませんでした。

従って、代替手段として上記の処理を切り出し、[Heroku](https://jp.heroku.com/)上にWebAPIとして分離することとしました。
（ソースコードは後述）

GASのみで実現できればベストだったのですが、現状ではどうしても限界があるようです。
herokuは無料で利用できるPaaSです。部分的に利用する分にはこれで十分と考えます。

# 処理フロー

処理全体のフロー図です。該当課題を以下のような構成で実現しました。

![処理フロー-Page-9.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/581277/2db78620-aa4a-9ad3-d8b9-81eeadde40fa.png)

# 成果物

今回の成果物です。

# WebAPI

PythonでWebAPI化した処理のソースコードです。

```python:main.py
import os
import io
import base64
import json

from flask import Flask, render_template, request, jsonify
from lxml import etree

from zipfile import ZipFile
from getpass import getpass



# 自分の名称を app という名前でインスタンス化
app = Flask(__name__)

# XSLT処理 XML/XSL→HTML
# [POST] /xslt
@app.route('/xslt', methods=['POST'])
def xslt():
    # XML/XSLの元データをbase64エンコードされたもので受け取る
    # デコードとしてUTF-8に変換
    xsl = base64.b64decode(str(request.form['xsl'])).decode('utf8')
    xml = base64.b64decode(str(request.form['xml'])).decode('utf8')

    # 変換
    xslt_root = etree.XML(xsl.encode('utf8'))
    transform = etree.XSLT(xslt_root)

    f = io.BytesIO(xml.encode('utf8'))

    doc = etree.parse(f)
    result_tree = transform(doc)

    # 処理結果(HTML)を文字列として返却
    return str(result_tree)

# zip解凍
# [POST] /unzip
@app.route('/unzip', methods=['POST'])
def unzip():
    # zipバイナリをbase64エンコードされたもので受け取る
    ds = base64.b64decode(str(request.form['data']))

    fi = io.BytesIO(ds)

    # レスポンス用のディクショナリ
    result = []

    # zip内のファイルを解凍
    with ZipFile(fi) as z:
        for zinfo in z.infolist():
            tmp_filename = zinfo.filename
            if zinfo.flag_bits & 0x1:
                password = getpass('PASSWORD: ')
                z.setpassword(password.encode('utf-8'))
            if not zinfo.flag_bits & 0x800:
                try:
                    name = zinfo.filename.encode('cp437').decode('cp932')
                except:
                    name = zinfo.filename.encode('cp437').decode('utf-8')
                zinfo.filename = name
            #print("Extracting", zinfo.filename)
            obj = {}
            fb = z.read(tmp_filename)
            fbe = base64.b64encode(fb)
            # ファイル名
            obj['name'] = base64.b64encode(zinfo.filename.encode()).decode()
            # ファイルのバイナリ文字列
            obj['data'] = fbe.decode()
            result.append(obj)
    
    # JSONで返却
    return jsonify(result)

if __name__ == '__main__':
	app.run(debug=True)
```

# おわりに
ここまでで、通知までを含む処理ができあがりました。

株式会社シクミヤでは、こうした課題の解決にこれからも取り組んで参ります。
最後までお付き合いいただき、ありがとうございました。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/581277/94fd031b-0c09-4f37-116f-c05271fdd7f9.png" width="200">
弊社は業務効率化・自動化など、仕組みで解決するお手伝いをさせていただいております。
お仕事のご依頼はコチラ↓までお願いいたします。

[株式会社シクミヤ](https://shikumiya.co.jp/)
note: [Visionary Base編集部](https://note.com/visionarybase)
Twitter: @shikumiya_hata