Try   HackMD

GAS 入門して請求書自動作成 Slack Bot 作りました

これは、HUITアドベントカレンダー2021の23日目の記事です。

こんにちは。HUIT 老害のもぎです。メリークリスマスですね!僕は野尻湖にあるサウナー界で超有名な(2カ月後までサウナの予約が埋まっている)サウナ&ゲストハウスで満喫します🧖‍♀️

https://sauna-ikitai.com/saunas/5357

さて、昨日は誰も投稿がありませんでした。20日には希望の星あるみかん様がwebページを作った投稿をしてくれました!

https://al-mikan.github.io/entry.html

記事の背景

僕は、最近業務委託で3社ぐらいの開発とか諸々をお手伝いさせていただいており、HUITにはなかなか顔を出せず(LT発表すらできず)悲しい気持ちです。

業務委託なので僕が請求書を毎月出してお金をいただく形になるのですが、どうも、毎回コピペしながら作るのが億劫になってきました。マネーフォワードfreee など便利なツールがいっぱい登場しているのですが、まだ個人事業主の開業届も出してないですし、お金もかかります。

そこで、Google App Script という Google のあらゆるツールを色々いい感じにできるプログラミング言語を用いて、請求書をコマンドで生成できるものを作りました。

Google App Scriptは「GAS(ガス)」とも呼ばれていて、JavaScriptベースで記述できる言語になります。天下のGoogleが提供しています。

コードはこちら(リファクタは気が向いたらやります)

アプリケーションの概要

コマンドはSlackで叩き、そのイベントを検知してGASにPOSTリクエストを送る、という構成にしました。

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

Slack 側は、Botの使用可能イベントを設定し、Tokenをたくさんもらっといて、RequestURL(今回の場合は、GASのコードのデプロイURL)をつけてあげれば大体大丈夫だったので、今回の記事では省略します。

当初はLINE Bot にしようとしていたのですが、「exportして!」と入力したらPDFで請求書を送るというのがMessaging APIではできなさそうだと途中でわかり断念しました😭

途中に知るのではなく、予めアーキテクチャについて頭の中でイメージしたら、必要なことが使うAPIで可能なのか、調べてからやりましょう!

GASの基礎について

ここで、GASについて軽く紹介します。GASは、Googleが提供しているものなら Gmail や Google Drive などほぼ何でも使えます。今回はGoogle Spreadsheetに連携しました。

GASは、下のように関数ベースで記述、実行します。SpreadsheetApp.getActiveSpreadsheet()とメソッドを叩くことで、今紐づいているスプレッドシート全体を取得することができます。もしGASがメールに紐づいてたりしたらこのような方法ではなく、IDから取得するなど違う取得方法になります。

また、getSheetByName()でシートの名前からシートを取得します。

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

そして、[対象シート].getRange('hogehoge:hogehoge')のようにシートの範囲を指定して、.getValue()したり.setValue()したりすればいいんです。たったこれだけ。非常に便利ですね〜

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

作った機能について

まず、予めこのフォーマットに従って自分の住所とか銀行口座とかを書いてあるだけのテンプレを作っておきます(これもいい感じにしたい)

/template (会社名) (時給) (郵便番号) (住所1) (住所2)

ある会社のテンプレートを作成することができました。

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

const create_template = (name, wage, postal_code, address, address_2) => { const sheet = SpreadsheetApp.getActiveSpreadsheet() const template_sheet = sheet.getSheetByName('template') if (template_sheet === null) { throw new Error("テンプレートがありません!テンプレートを作成しましょう") } const new_sheet = sheet.insertSheet('template_' + name, 0, {template: template_sheet}); new_sheet.getRange('B1').setValue('〒' + postal_code) new_sheet.getRange('B2').setValue(address) new_sheet.getRange('B3').setValue(address_2) new_sheet.getRange('B4').setValue(name) new_sheet.getRange('D11').setValue(wage) const BaseURL = "https://docs.google.com/spreadsheets/d/" + sheet.getId() + "/edit?gid=" + new_sheet.getSheetId() reply_text(BaseURL) } 

こんな感じで無理やりセットする感じです。sheet.insertSheet()でシートをコピーしています。

/record (会社名) (就業時間) (日にち、指定しなくてもよい)

このコマンドで、今日働いた時間の分をシートに追加してもらえます。その月の請求書がなかったら自動でその月の分を作成してくれるようにしました。

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

const num = 11 + exist_columns.length sheet.insertRows(11 + exist_columns.length, 1) const wage = sheet.getRange('D11').getValue() sheet.getRange('A' + num + ':E' + num).setValues([[formatted_date,'自宅作業', working_hour, wage, working_hour*wage]])

こうして、無理やり行数を数えて下に追加している感じで、絶対もっといい感じのやり方がある(はず)

/export (会社名) (年月)

指定した年月のシートをPDFにしてSlackに送ってくれます!非常に便利!

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

const BaseURL = "https://docs.google.com/spreadsheets/d/" + spread_id + "/export?gid=" + sheet_id // PDF出力する時のオプションを指定できる const pdfOptions = "&exportFormat=pdf&format=pdf&size=A4&portrait=true&fitw=true&top_margin=0.50&right_margin=0.50&bottom_margin=0.50&left_margin=0.50&horizontal_alignment=CENTER&vertical_alignment=TOP&printtitle=false&sheetnames=false&gridlines=false&fzr=false&fzc=false" const URL = BaseURL + pdfOptions const access_token = ScriptApp.getOAuthToken() const options = { headers: { 'Authorization': 'Bearer ' + access_token } }; //PDFを作成する const blob = UrlFetchApp.fetch(URL, options).getBlob().setName(name + '.pdf'); // SlackのAPIを叩く時のフォーマットで、channelとか指定できる。 const params = { "method" : "post", "payload" : { "token" : token, "channels": "#working", "file": blob } }; const res = UrlFetchApp.fetch("https://slack.com/api/files.upload", params) // GCPのログに保管しておきます。 Logger.log(res)

機能を作成するにあたって、必要なTokenなどは、事前に環境変数みたいな感じで登録することができました。https://developers.google.com/apps-script/guides/properties

const prop = PropertiesService.getScriptProperties(); const token = prop.getProperty("bot_token") const verification_token = prop.getProperty("verification_token")

最後に

急いで必死に作ったので、改善点ばっかりですが、久しぶりに新しいものに入門した気がして楽しかったです。

非常に簡単にいい感じのものを作成することができたので、ぜひ皆さんもGASを触ってみてください!