Edited at

Google Apps Scriptで支払業務の物忘れを防いだお話


はじめに

初Qiita投稿です。

こちらでのお作法とかをあまりよくわかっていないのですが、雰囲気を探っていただけますと幸いです。


著者プロフィール


  • 文系大学生から会計系専門ファームに就職したため、プログラミングとは無縁だった

  • プログラミングはほぼやったことがなかったが、2017年の8月くらいから独学でGAS,Pythonを書くようになった

  • ネイティブなエンジニアではないが、ググったりしながら何となくコードを書けるようになってきたにわかコーダー(良い書き方があれば教えてください。。。)

  • 最近はPythonのデータサイエンス的なところも勉強中です()


著者の基本思想


  1. 定型的な業務を避けたい

  2. 一回一回の作業量が少ない仕事でも、やるまでの精神的コストが高い仕事は極力避けたい

  3. 人間がやる仕事じゃないだろ?とおもった仕事はマジでやる気失う


自動化した業務の内容


従来の業務


業務内容


  1. 請求書が指定のメールアドレスに送られる


    1. その中から請求書ファイルを取り出す(だるい)

    2. 取り出したファイルをGoogleドライブに格納する(なんで俺がやるんだ?)



  2. 支払担当者が未払いの請求書を確認して、振込業務を行うか検討する


    1. その際に窓口担当者が支払担当者に通知を行うがわすれがち(にんげんだもの!)

    2. 支払担当者もアナウンスが無い限り支払を忘れる(にんげんだもの!!)



  3. 実際の振込業務を行う(freeeAPIを使えば出来るんだけど、今回は割愛!)


    1. 銀行のサイトにアクセスする

    2. 支払依頼をする

    3. 指定日に振り込まれる




プログラムの作成動機

前提として、私は非常に物忘れの激しいかつめんどくさがりな人間です。

だからこそ、退屈で面白みのない仕事は仕組み化して可能な限り避けるようにしたいという思想は人一倍強いです。

そのため、単純な作業を文句も言わずにやってくれるプログラミング頼りたいと思い、このシステムを作りました。


自動化後の業務


  • GASで指定した請求書ファイルを、指定のフォルダに自動格納(日時)

  • 未払の請求書の有無をSlackで自動リマインドする(平日)

上記1及び2の業務で人間がやる必要のある箇所を削ることが出来ました!


コード


Gmailからデータを取得する場合のコード


//前提情報。削除禁止
var mailAddress = "〇〇@gmail.com"; //pdfを取得する対象のメールアドレスを取得する
var folderId = "〇〇";//googleドライブのフォルダーIDを記載

//検索する日付の指定。今回は前日来たメールを取得するので、変更したい場合は2行目のgetDate()のあとの数字をいじる
var today = new Date();
today.setDate(today.getDate()-1);
var inputday = Utilities.formatDate(today, 'JST', 'yyyy/MM/dd')//todayのデータを整形する

//検索窓に入れる単語を指定する。
var searchDate = 'after:'+inputday;
var searchAddress = 'to:'+ mailAddress;
var searchTerm = searchDate +' '+ searchAddress; //gmailの検索ワードがsearchTermとして検索される。
//例えば、2019/2/28に実行した場合は、Gmailの検索窓に「after:2019/2/28 to:◯◯@gmail.com」と検索した結果が返ります。

//フォルダとスレッドから、pdfデータのみを取得してgoogleドライブに格納する
function moveFromBillingMail() {
var folder = DriveApp.getFolderById(folderId);
var thread = GmailApp.search(searchTerm,0,10);
var myMessages = GmailApp.getMessagesForThreads(thread);//配列表記になっているのは、[スレッド][スレッド内のメッセージの順]

//ここからforループで検索で取得したメールの中から全てのファイルを取得すうr
for(var i = 0; i <= thread.length; i++){
for(var j in myMessages[i]){
var attachments = myMessages[i][j].getAttachments(); //メッセージについているすべての添付ファイルを取得
for(var k in attachments){
folder.createFile(attachments[k]); //ドライブに添付ファイルを保存
}
}
}
}


指定のフォルダに格納したファイル名を取得して、Slackに投下する

function alartPayment(){

var folder = DriveApp.getFolderById(folderId).getFiles();
var names = [];

//このfor文を使うことによって、フォルダの中にあるファイル名をすべて取得する
for (var i = 0 ; folder.hasNext(); i++){
var file = folder.next();
var name = file.getName();
names[i] = name;
var message = names[i];
postSlack(message);
}
}

//alartPaymentを実行するために
function postSlack(message){
var url = 'https://hooks.slack.com/services/◯◯'; //予めslackWebAPIから、送信用URLを取得しておく。

var jsonData = {
"text" : message
};

var payload = JSON.stringify(jsonData);

var options = {
"method":"post",
"contentType":"application/json",
"payload":payload
}
UrlFetchApp.fetch(url, options);
}


使用したAPI


  • SlackAPIのうち、Incoming Webhook


APIの入手場所

ここの右上にある Your Appsから、Create New Appを選択してください

https://api.slack.com/

そこからアプリ名と投稿したいチャンネルを選択して

Incoming Webhooksを選択


incomingwebhookから、WebhookURLを取得する


Slackのアプリ名及びアイコンについて

Display Infomationから選択するのが一番簡単なので、そうしました


実際に投稿された内容がこちら

スクリーンショット 2019-03-06 16.54.30.png

今回の場合は3件のpdfを取得したので、3件結果がリターンされました。


参考にしたサイト

基本的にはAPIドキュメントを使いました。

https://api.slack.com/tutorials

https://developers.google.com/apps-script/reference/


最後に

このプログラムはあくまで最低限の動作を行うためのものなので、追記とかしたいのでアレばガンガンやっちゃってください。

著者自身はプログラマーとしては全然ひよっ子なので、コメントいただける方は是非ともご指導のほどお願い申し上げます。


20190405追記

友人エンジニアから、お前のコードは動くけどツッコミどころ、書き方の作法がなってないと指摘を受けました。

そのため、猛省して良い書き方を修正しました。

var folderId = "任意のもの";

var folder = DriveApp.getFolderById(folderId);
var files = folder.getFiles();

function moveFromBillingMail() {

//即時関数を使ってyesterdayを簡単に使う
var yesterday = (function(){
var d = new Date(); //この時点はでtoday
d.setDate(d.getDate()-1);
return Utilities.formatDate(d, 'JST',' yyyy/MM/dd');
})();

function moveAttachmentsFromMessage(message){
var attachments = message.getAttachments();
attachments.forEach(function(f){
folder.createFile(f);
});
}
//Gmailから検索ワードを使って取り出す
var threads = (function (){
var mailAddress = "◯◯@gmail.com";
var searchDate = 'after:'+ yesterday;
var searchAddress = 'to:'+ mailAddress;
var searchTerm = searchDate+ ' ' + searchAddress;
return GmailApp.search(searchTerm,0,10);
})();

threads.map(function(thread){
var messages = GmailApp.getMessagesForThread(thread);
messages.map(moveAttachmentsFromMessage);
});
}

//slackに実際のアラートを行うための関数
function alartPayment(){
if (!files.hasNext()){
postSlack('支払ってない請求書はゼロです。やるじゃん。');
return;
}
while(files.hasNext()){
postSlack(files.next().getName());//テキスト名を取得して終わり。
}
}

function postSlack(message){
var url = 'https://hooks.slack.com/services/〇〇';
var jsonData = {
"text" : message
};
var payload = JSON.stringify(jsonData);
var options = {
"method":"post",
"contentType":"application/json",
"payload":payload
}
UrlFetchApp.fetch(url, options);
}


変更点


即時関数を使ってスコープを小さくする

yesterdayの部分については一回しか使わないので、その部分は即時関数としてカプセル化することで他の変数や関数に影響を与えないようにするという意図です。

今回の場合だと、以下の変数に対して即時変数を使いました

  //yesterdayを表す部分

var yesterday = (function(){
var d = new Date(); //この時点はでtoday
d.setDate(d.getDate()-1);
return Utilities.formatDate(d, 'JST',' yyyy/MM/dd');
})();

//Gmailの検索の部分

var threads = (function (){
var mailAddress = "◯◯@gmail.com";
var searchDate = 'after:'+ yesterday;
var searchAddress = 'to:'+ mailAddress;
var searchTerm = searchDate+ ' ' + searchAddress;
return GmailApp.search(searchTerm,0,10);
})();


高階関数を取り扱う

高階関数とは「関数を引数、戻り値として扱う関数」のことです。

mapやforEachにより、配列内の要素を順番に関数で処理したりに使いました。

これにより、ネストを深くfor文を書かなくてもよかったりするからスッキリコードがまとまりました。

  //メッセージから添付ファイルを取得して、配列の内容全てからファイルをドライブのフォルダーに作る。

function moveAttachmentsFromMessage(message){
var attachments = message.getAttachments();
attachments.forEach(function(f){
folder.createFile(f);
});

//スレッドを一つ一つ取得して、取得したスレッド一つ一つに、moveAttachmentsFromMessageを実行

threads.map(function(thread){
var messages = GmailApp.getMessagesForThread(thread);
messages.map(moveAttachmentsFromMessage);
});


論理演算は人が理解しやすいように書く

If文の論理構造は基本的に人が話してスッキリ伝わるように書くのがわかりやすいということです。

下記の文章は、人間がスッキリと理解しやすいですよね。

- フォルダにファイルが存在しなかったら、'支払ってない請求書はゼロです。やるじゃん。'とポスト

- そうじゃないなら(=ファイルがあるなら)全部ファイル名をSlackにポストする

だから、人間の感性に沿った論理構造でやりましょう。というお話。

//slackに実際のアラートを行うための関数

function alartPayment(){
if (!files.hasNext()){
postSlack('支払ってない請求書はゼロです。やるじゃん。');
return;
}
while(files.hasNext()){
postSlack(files.next().getName());//テキスト名を取得して終わり。
}
}


追記

エンジニアにボコボコにされましたが、本当に納得だわ。

同じ内容でもここまで書き方が変わるなんて。

引き続き自己研鑽してみます!