はじめに
私の勤務先では、 数年前より給与計算に人事労務freee、出退勤時刻の記録にオープンソースの「みやもとさん」を設置して使用しています。
- masuidrive/miyamoto: Google Apps Scriptで書かれたSlack用勤怠管理Botの「みやもとさん」
- チャットで勤怠管理する「みやもとさん」をリリースしました – @masuidrive blog
freee人事労務を導入した当初は、freee公式の打刻APIが使用できないプランで契約していたのと、みやもとさん単体では、時間外労働時間の計算に対応できない課題がありました。そこで、 みやもとさんのGoogle SpreadSheetからGASで出退勤時間を取得し、人事労務freee APIを使って登録するGoogle Apps Scriptを作成しました。
- これから新規でslackによる勤怠管理を導入するなら、freee公式の打刻Appを導入するほうが楽だと思います。
- 既にみやもとさんを導入済みの場合に役立つかもしれません
処理の概要
slackで出勤時に「おはよう」、退勤時に「おつかれさまでした」とつぶやく → みやもとさんが Google SpreadSheetに出退勤時間を記録 → 今回作ったGoogle Apps Scriptを毎日午前2時に定時実行 →人事労務freeeに転記
前提
- みやもとさんは設置済み
- freee APIにアプリを登録して、APP IDとAPP SECRETを取得している
- ものとします
実装
- Google Scriptのスクリプトエディタを開き、コードをコピーします。
- スクリプトのプロパティに秘匿情報を追加します。
- APP_ID freee APIのAPP_ID
- SECRET freee APIのSECRET
- EMPLOYEE シート名と従業員IDの対応をJSONで記述 例){"kkanazaw": "12345"}
- MIYAMOTO_SAN_ID みやもとさんのGoogle SpreadSheetのid
- COMPANY_ID freeeの企業ID
- スクリプトを定期実行するトリガーを設定をします
OAuth2認証の部分は【freee API】GASを用いてGoogleスプレッドシートと連携する – freee ヘルプセンターのサンプルコードを使用しています。
function myFunction() {
var company_id = PropertiesService.getScriptProperties().getProperty("COMPANY_ID");
// スクリプトのプロパティEMPLOYEEに連携するシート名とfreeeの従業員IDの対応をJSONで記述する
// シート名:freeeの授業員ID
// シート名とfreeeの授業員IDは紐付いていないため、手動で設定しないといけない。
// 従業員IDはFreeeの従業員一覧APIから取得できる。
// APIリファレンスのページで実際にAPIを叩けるので、
// getAccessToken()でaccesstokenを取得し、company_id,年,月のパラメータを指定して実行する。
// API
// https://developer.freee.co.jp/docs/hr/reference#/%E5%BE%93%E6%A5%AD%E5%93%A1/index2
var config = JSON.parse(PropertiesService.getScriptProperties().getProperty("EMPLOYEE"));
for (var key in config) {
var emp_id = config[key];
var today = new Date();
var year = Utilities.formatDate( today, 'Asia/Tokyo', "yyyy");
var month = Utilities.formatDate( today, 'Asia/Tokyo', "MM");
var spreadsheet = SpreadsheetApp.openById(PropertiesService.getScriptProperties().getProperty("MIYAMOTO_SAN_ID"));
var sheet = spreadsheet.getSheetByName(key);
var data = sheet.getDataRange().getValues();
for(i = 1; i <= 31; i++) {
var latest = data[data.length-i];
var date = latest[0];
var start = latest[1];
var end = latest[2];
if(date && start && end) {
putRequest(company_id, emp_id, date, start, end);
Utilities.sleep(1000);
}
}
}
}
/******************************************************************
参照ライブラリ
title |OAuth2
project_key |1B7FSrk5Zi6L1rSxxTDgDEUsPzlukDsi4KGuTMorsTQHhGBzBkMun4iDF
******************************************************************/
//連携アプリ情報(Googleスプレッドシートサンプルファイル)
var APP_ID = PropertiesService.getScriptProperties().getProperty("APP_ID");
var SECRET = PropertiesService.getScriptProperties().getProperty("SECRET")
/******************************************************************
function name |alertAuth
summary |認証のエンドポイントとなるダイアログ
******************************************************************/
function alertAuth() {
var service = getService();
var authorizationUrl = service.getAuthorizationUrl();
console.log(authorizationUrl);
}
/******************************************************************
function name |getService
summary |freeeAPIのサービスを取得
******************************************************************/
function getService() {
return OAuth2.createService( "freee" )
.setAuthorizationBaseUrl( "https://accounts.secure.freee.co.jp/public_api/authorize" )
.setTokenUrl( "https://accounts.secure.freee.co.jp/public_api/token" )
.setClientId( APP_ID )
.setClientSecret( SECRET )
.setCallbackFunction( "authCallback" )
.setPropertyStore( PropertiesService.getUserProperties() );
}
/******************************************************************
function name |authCallback
summary |認証コールバック
******************************************************************/
function authCallback( request ) {
var service = getService();
var isAuthorized = service.handleCallback( request );
if ( isAuthorized ) {
return HtmlService.createHtmlOutput( "認証に成功しました。タブを閉じてください。" );
} else {
return HtmlService.createHtmlOutput( "認証に失敗しました。" );
};
}
function putRequest(company_id, emp_id, date, start, end) {
var freeeApp = getService();
var accessToken = freeeApp.getAccessToken();
var requestUrl = "https://api.freee.co.jp/hr/api/v1/employees/" + emp_id + "/work_records/" + Utilities.formatDate( date, 'Asia/Tokyo', 'yyyy-MM-dd');
var headers = {"Authorization" : "Bearer " + accessToken };
var requestBody = {
"company_id": company_id,
"break_records": [
{
"clock_in_at": Utilities.formatDate( date, 'Asia/Tokyo', "yyyy-MM-dd'T''12:00:00.000'Z"),
"clock_out_at": Utilities.formatDate( date, 'Asia/Tokyo', "yyyy-MM-dd'T''13:00:00.000'Z")
}
],
"clock_in_at": Utilities.formatDate( start, 'Asia/Tokyo', "yyyy-MM-dd'T'HH:mm:ss.000Z"),
"clock_out_at": Utilities.formatDate( end, 'Asia/Tokyo', "yyyy-MM-dd'T'HH:mm:ss.000Z")
};
diff_time = (end.getTime() - start.getTime()) / (1000 * 60 * 60);
if(diff_time < 6.0) {
delete requestBody["break_records"];
}
var options = {
"method":"PUT",
"headers":headers,
'contentType': 'application/json',
"payload":JSON.stringify(requestBody),
muteHttpExceptions: true
};
var res = UrlFetchApp.fetch( requestUrl , options );
console.log(res);
}
// debug用
// APIリファレンスの画面でemp_idやcompany_idを取得したいときに使う
// https://developer.freee.co.jp/docs/hr/reference
function getAccessToken() {
var freeeApp = getService();
console.log(freeeApp.getAccessToken());
}
実行結果
みやもとさんのGoogle SpreadSheetに記録された出退勤時間が
スクリプト実行後に、前日までの出退勤時刻がFreee側に転記されます。
- 時間外労働時間があった場合も、Freee側で計算されます。
課題
- 毎回過去31日分の勤怠を更新しています
- みやもとさん側で過去の勤怠記録を編集した場合や、たまたまスクリプトがエラーになった場合に備え、翌日の処理で更新できるようにしています。そのためやや冗長な書き方になっています。
- 従業員数が増えた場合は、APIのリクエスト数の調整が必要です。
- シート名と従業員IDの登録部分
- 人事労務freeeに登録しているメールアドレスと、slackのアドレスが違うケースがあるため、手動でドキュメントプロパティに登録しています。
- アドレスを統一できるなら、従業員APIから自動的に設定できると思います。
- 人事労務freeeに登録しているメールアドレスと、slackのアドレスが違うケースがあるため、手動でドキュメントプロパティに登録しています。