shojiです。
今回は、Google Apps Script(GAS)を使い、BoardというバックオフィスツールとSlackを連携して、月末に請求情報をSlack通知する仕組みを作ってみたので、その実装方法をブログにしてみます。今回は第2回(全2回)の実装編です。
実装内容
前回、準備作業が完了したので、今回はGoogle Apps Scriptのプログラムコードを書いていきます。
ファイルの構成とそれぞれの処理内容は次のようにします。
- Board.gs(Boardクラス)
- Board APIを実行し、パラメータで指定された期間に請求日が含まれる全ての請求情報を取得する。
- Board APIは1回で100件の取得上限があるため、100件以上データがある場合はループで何度も取得する。
- 取得したデータから、Slackへ投稿するために必要なデータのみをオブジェクトに格納して配列で返す。
- Slack.gs(Slackクラス)
- 請求データを整形し、SlackのエンドポイントへPOSTデータとして送信する。
- Utility.gs(Utilityクラス)
- 現在日付から月初(文字列)を返す関数。
- 現在日付から月末(文字列)を返す関数。
- 現在日付からその月の最終の平日を返す関数(ただし土日対応のみ。祝日対応はしない。)
- Main.gs(メイン処理)
- 今日が実行対象日(月の最後の平日)ではなかったら、何もせず終了する。
- 今日が実行対象日だったら、Boardから請求情報を取得してSlackへ送信する。
- この関数をトリガーで毎日指定時間に実行させる。
では早速それぞれのソースコードファイルを書いていきます。
Boardクラス
Google Apps Scriptの「ファイルを追加」ボタン(+)から、「スクリプト」を選択して、名前をつけてからコードを書きていきます。
まず、BoardのAPIから請求情報を取得して返却するBoardクラスをズバっと書きます。
API仕様についてはBoard APIオンラインドキュメントを参考にしながら記述します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 |
/** * Boardデータ取得用クラス */ class Board { /** * 定数 */ static get END_POINT() { return 'https://api.the-board.jp/v1/'; } static get PER_PAGE() { return 100; } /** * 請求データ一覧取得 * 受注確定または受注済みの請求データ一覧を取得する */ static getInvoiceList(fromDate, toDate) { const headers = { 'Authorization': 'Bearer ' + PropertiesService.getScriptProperties().getProperty('API_TOKEN'), 'x-api-key': PropertiesService.getScriptProperties().getProperty('API_KEY') }; const options = { 'headers': headers }; let page = 1; let current_per_page = 0; let x_total_count = 0; const responses = []; // 対象となるレコード数をすべて取得するまでループ do { // URI生成 const uri = Board.END_POINT + `invoices?invoice_date_gteq=${fromDate}&invoice_date_lteq=${toDate}&project_order_status_in[]=4,5&page=${page}&per_page=${Board.PER_PAGE}`; Logger.log(uri); // API実行 const response = UrlFetchApp.fetch(uri, options); responses.push(response.getContentText()); // トータル件数取得 x_total_count = response.getHeaders()['x-total-count']; current_per_page += Board.PER_PAGE; page++; } while(current_per_page < x_total_count) // 整形して戻す return Board._shapingInvoiceList(responses); } /** * 請求データ整形、全ページデータを1つの配列へ格納 */ static _shapingInvoiceList(responses) { const result = []; for (let i=0; i<responses.length; i++) { const json_data = JSON.parse(responses[i]); for (let j=0; j<json_data.length; j++) { result.push({ 'project_id' : json_data[j]['project_id'], 'project_no' : json_data[j]['project_no'], 'project_name' : json_data[j]['name'], 'client_name' : json_data[j]['client']['name_disp'], 'total' : json_data[j]['total'], 'tax' : json_data[j]['tax'], 'order_status_name' : json_data[j]['order_status_name'], 'invoice_date' : json_data[j]['invoice_date'], 'invoice_status_name' : json_data[j]['invoice_status_name'], 'project_type3_name' : json_data[j]['project_type3_name'], 'group_name' : json_data[j]['group_name'] }); } } return result; } } |
Slackクラス
続いて、Slackのエンドポイントへ請求情報データを送信するためのプログラムをズババっと書きます。
Slackへ送信するデータ構造については、Slack公式のBlock Kit Builderというツールを使って作ると便利です。
定数の*WebHookURL*は第1回(準備編)でメモしておいたエンドポイントURLに置き換えてください。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 |
/** * Slack投稿用クラス */ class Slack { /** * 定数 */ static get SLACK_WEBHOOK_URL() { return '*WebHookURL*'; } static get USER_NAME() { return 'sris'; } /** * Slack通知 */ static sendNotification(responses) { // メッセージ生成 const divider = { 'type': 'divider' }; const invoices = [divider]; for (let i=0; i<responses.length; i++) { const invoice = { 'type': 'section', 'fields': [ { 'type': 'mrkdwn', 'text': `*案件名*\n<https://the-board.jp/projects/${responses[i].project_id}/edit|${responses[i].project_name}>` }, { 'type': 'mrkdwn', 'text': `*クライアント:*\n${responses[i].client_name}` }, { 'type': 'mrkdwn', 'text': `*請求日:*\n${responses[i].invoice_date}` }, { 'type': 'mrkdwn', 'text': `*請求ステータス:*\n${responses[i].invoice_status_name}` } ] }; invoices.push(invoice); invoices.push(divider); } // タイトル(兼PUSH通知メッセージ) const subjectText = '<!channel> 今月の<https://the-board.jp/invoices|請求情報>でリス!'; const payload = { 'username': Slack.USER_NAME, 'text': subjectText, 'blocks': [ { 'type': 'section', 'text': { 'type': 'mrkdwn', 'text': subjectText } } ] }; payload['blocks'] = payload['blocks'].concat(invoices); const options = { 'method': 'POST', 'contentType': 'application/json', 'payload': JSON.stringify(payload) }; UrlFetchApp.fetch(Slack.SLACK_WEBHOOK_URL, options); } } |
Utilityクラス
日付処理を行う便利メソッドを書きます。
今回作成するのは、次の3つの関数です。
- パラメータのdateオブジェクトから、その月の月初の日付(YYYY-MM-DD)文字列を返す関数
- パラメータのdateオブジェクトから、その月の月末の日付(YYYY-MM-DD)文字列を返す関数
- パラメータのdateオブジェクトから、その月の最後の平日のdateオブジェクトを返す関数
それぞれ実装していきます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
/** * ユーティリティクラス */ class Utility { /** * dateから月初(YYYY-MM-DD)を返す */ static getFirstDayOfMonthString(now) { const tmpNow = new Date(now.getTime()); tmpNow.setDate(1); tmpNow.setHours(0, 0, 0, 0); return `${tmpNow.getFullYear()}-${(tmpNow.getMonth()+1).toString().padStart(2, '0')}-${tmpNow.getDate().toString().padStart(2, '0')}`; } /** * dateから月末(YYYY-MM-DD)を返す */ static getLastDayOfMonthString(now) { const tmpNow = new Date(now.getTime()); tmpNow.setMonth(tmpNow.getMonth() + 1); tmpNow.setDate(0); tmpNow.setHours(0, 0, 0, 0); return `${tmpNow.getFullYear()}-${(tmpNow.getMonth()+1).toString().padStart(2, '0')}-${tmpNow.getDate().toString().padStart(2, '0')}`; } /** * dateから最終平日(dateオブジェクト)を返す */ static getLastWeekDayOfMonth(now) { const tmpNow = new Date(now.getTime()); tmpNow.setMonth(tmpNow.getMonth() + 1); tmpNow.setDate(0); tmpNow.setHours(0, 0, 0, 0); // 土日対応 if (tmpNow.getDay() === 0) { tmpNow.setDate(tmpNow.getDate()-2); } else if (tmpNow.getDay() === 6) { tmpNow.setDate(tmpNow.getDate()-1); } return tmpNow; } } |
メイン処理
最後にメイン処理のプログラムを書きます。
実行対象日(月の最後の平日)であればSlack通知を行い、そうでなければログ出力のみを行います。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
function myFunction() { // 通知する年月 const _now = new Date(); const lastWeekDay = Utility.getLastWeekDayOfMonth(_now); const fromDateString = Utility.getFirstDayOfMonthString(_now); const toDateString = Utility.getLastDayOfMonthString(_now); // 最終平日(祝日判定無し)に実行する if (_now.getDate() === lastWeekDay.getDate()) { // Boardから請求データを取得 const responses = Board.getInvoiceList(fromDateString, toDateString); // Slackへ通知 Slack.sendNotification(responses); } else { Logger.log('対象日ではないためキャンセルします。') } } |
テスト実行
ここまで作成したら、ちゃんと動くかテストします。
Google Apps Scriptでメイン処理のmyFunction関数を実行します。
実行した日が月の最終の平日であれば、Incoming Webhookの設定で指定したSlackチャンネルへ、当月が請求日の請求情報が投稿されるはずです。
トリガーの設定
最後に、図1のように毎日午前10-11時に定期実行されるように、トリガーを設定します。
トリガーで月の最終の平日に実行!・・・と指定できればよいのですが、そういう設定はできないため、毎日10-11時に実行するようにトリガーを設定し、プログラム側で平日かどうか判定することで実現しています。
まとめ
以上で、実行編は終了となります。
Boardに限らず、データを取得できるサービスであれば同様の仕組みは作れると思いますので、ぜひこの記事を参考にオリジナルのSlack Botを作ってみてください。
それでは!