みやもとさんで記録した勤務時間をGASで人事労務freeeに転記する

# はじめに
私の勤務先では、 数年前より給与計算に人事労務freee、出退勤時刻の記録にオープンソースの「みやもとさん」を設置して使用しています。

- [masuidrive/miyamoto: Google Apps Scriptで書かれたSlack用勤怠管理Botの「みやもとさん」](https://github.com/masuidrive/miyamoto)
- [チャットで勤怠管理する「みやもとさん」をリリースしました – @masuidrive blog](http://blog.masuidrive.jp/2014/10/19/miyamoto-san/)

freee人事労務を導入した当初は、freee公式の打刻APIが使用できないプランで契約していたのと、みやもとさん単体では、時間外労働時間の計算に対応できない課題がありました。そこで、 みやもとさんのGoogle SpreadSheetからGASで出退勤時間を取得し、人事労務freee APIを使って登録するGoogle Apps Scriptを作成しました。<br>

- これから新規でslackによる勤怠管理を導入するなら、freee公式の打刻Appを導入するほうが楽だと思います。
- 既にみやもとさんを導入済みの場合に役立つかもしれません

# 処理の概要
slackで出勤時に「おはよう」、退勤時に「おつかれさまでした」とつぶやく → みやもとさんが Google SpreadSheetに出退勤時間を記録 → 今回作ったGoogle Apps Scriptを毎日午前2時に定時実行 →人事労務freeeに転記<br>

# 前提

- みやもとさんは設置済み
- freee APIにアプリを登録して、APP IDとAPP SECRETを取得している
    - ものとします

# 実装
1. Google Scriptのスクリプトエディタを開き、コードをコピーします。
2. スクリプトのプロパティに秘匿情報を追加します。
  - 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
3. スクリプトを定期実行するトリガーを設定をします

OAuth2認証の部分は[【freee API】GASを用いてGoogleスプレッドシートと連携する – freee ヘルプセンター](https://support.freee.co.jp/hc/ja/articles/115005287286--freee-API-GAS%E3%82%92%E7%94%A8%E3%81%84%E3%81%A6Google%E3%82%B9%E3%83%97%E3%83%AC%E3%83%83%E3%83%89%E3%82%B7%E3%83%BC%E3%83%88%E3%81%A8%E9%80%A3%E6%90%BA%E3%81%99%E3%82%8B)のサンプルコードを使用しています。<br>

```javascript
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に記録された出退勤時間が<br>
[![Image](https://i.gyazo.com/3c7eb3cc848a394eb72acfe6788e97a9.png)](https://i.gyazo.com/3c7eb3cc848a394eb72acfe6788e97a9.png)<br>

スクリプト実行後に、前日までの出退勤時刻がFreee側に転記されます。<br>
    - 時間外労働時間があった場合も、Freee側で計算されます。
[![Image](https://gyazo.com/fe27781ac95387629bd9d83ade609b16/thumb/1000)](https://gyazo.com/fe27781ac95387629bd9d83ade609b16)<br>


# 課題
- 毎回過去31日分の勤怠を更新しています
  - みやもとさん側で過去の勤怠記録を編集した場合や、たまたまスクリプトがエラーになった場合に備え、翌日の処理で更新できるようにしています。そのためやや冗長な書き方になっています。
  - 従業員数が増えた場合は、APIのリクエスト数の調整が必要です。
- シート名と従業員IDの登録部分
  - 人事労務freeeに登録しているメールアドレスと、slackのアドレスが違うケースがあるため、手動でドキュメントプロパティに登録しています。
    - アドレスを統一できるなら、従業員APIから自動的に設定できると思います。