freee 会計のAPI経由で証憑データを取得するサンプル

「freee会計に登録した証憑データをまとめてダウンロードしたい」という声を聞き、試しに作ってみました。

## やること

freee会計 API 経由で、事業所に保存されている証憑データをダウンロードするサンプルを作る。
実装はGoogle Apps Script で行い、データはGoogle Drive に保存する。

## 画面イメージ

スクリプトを実行すると、Googleスプレッドシートに操作用メニューが表示される。認可処理後「証憑を保存」をクリックすると、データ取得が始まる。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83767/abe05265-68d5-3c3a-078e-aed5031cc141.png" width="480">

取得したデータは、任意の　Google ドライブフォルダに保存される。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83767/ef51c242-84d1-df2a-9e4f-b59f68540023.png" width="480">

## 実装手順

以下の手順で実装・操作しました。

- freeeアプリの作成
- OAuth 2.0 ライブラリを Google スプレッドシートに設定
- Google Apps Script を記述
- 証憑を保存したいフォルダを作成し、フォルダIDを取得
- スプレッドシートに、必要な情報を記述する
- 認可後、保存実行

## freeeアプリの作成と、OAuth 2.0 ライブラリを Google スプレッドシートに設定

手順は、拙稿とほぼ同じです。こちらの記事をご覧ください。

[Google Apps Script + OAuthライブラリで、freee APIの操作を行う](https://qiita.com/TakeshiNickOsanai/items/62810b0e96bf37bd0eca)

freeeアプリ作成の際、権限設定「事業所」「ファイルボックス」にチェックを入れます。

## Google Apps Script を記述

スプレッドシートの「スクリプトエディタ」から、コードを記述します。以下は拙作のサンプルです。

```javascript
// 初期データを定義する
const Sheets = SpreadsheetApp.getActiveSheet();

const Client_ID = Sheets.getRange('B1').getValue();
const Client_Secret = Sheets.getRange('B2').getValue();
const Company_ID = Sheets.getRange('B3').getValue();
const Start_Date = Utilities.formatDate(Sheets.getRange('B4').getValue(), "JST", "yyyy-MM-dd");
const End_Date = Utilities.formatDate(Sheets.getRange('B5').getValue(), "JST", "yyyy-MM-dd");
const Limit = Sheets.getRange('B6').getValue();
const FolderID = Sheets.getRange('B7').getValue()


// freee API 通信時に必要な Bearer Token を定義してパラメータとして渡す
function header(){
  const params = {
    headers: {
      Authorization: 'Bearer ' + getDriveService().getAccessToken(),
    },
  }
  return params;
}

// 認可周りの処理を行う

function getDriveService() {

    return OAuth2.createService('freeeAPI')
        .setAuthorizationBaseUrl(
        　　'https://accounts.secure.freee.co.jp/public_api/authorize'
        )
        .setTokenUrl(
        　　'https://accounts.secure.freee.co.jp/public_api/token')
        .setClientId(
            Client_ID
        )
        .setClientSecret(
            Client_Secret
        )
        .setCallbackFunction('authCallback')
        .setPropertyStore(PropertiesService.getUserProperties());
}

// Google スプレッドシートに操作メニューを拡張

function onOpen() {
    SpreadsheetApp.getUi()
        .createMenu('freee API連携')
        .addItem('認可処理', 'showAuth')
        .addItem('事業所確認', 'showCompanies')
        .addItem('証憑を保存', 'getReceipts')
        .addItem('ログアウト', 'logout')
        .addToUi();
}

function createModelessDialog(html, title) {
    const htmlOutput = HtmlService.createHtmlOutput(html)
        .setWidth(360)
        .setHeight(160);
    SpreadsheetApp.getUi().showModelessDialog(htmlOutput, title);
}

// OAuth の認可処理

function showAuth() {
    if (!getDriveService().hasAccess()) {
        let authorizationUrl = getDriveService().getAuthorizationUrl();
        let template = HtmlService.createTemplate(
            '<a href="<?= authorizationUrl ?>" target="_blank">Authorize</a>. ' +
            'freee APIの認可をします。'
        );
        template.authorizationUrl = authorizationUrl;
        let page = template.evaluate();
        let title = 'freeeアプリの認可処理';

        createModelessDialog(page, title);
    } else {
        showUser();
    }
}

function authCallback(request) {
    const isAuthorized = getDriveService().handleCallback(request);
    if (isAuthorized) {
        return HtmlService.createHtmlOutput('Success! You can close this tab.');
    } else {
        return HtmlService.createHtmlOutput('Denied. You can close this tab');
    }
}

// 認可中のユーザー情報を表示する

function showUser() {

    let response = UrlFetchApp.fetch('https://api.freee.co.jp/api/1/users/me',header());
    let myUser = JSON.parse(response);
    Browser.msgBox('OAuth認可済みです。\\n認可されたユーザー名：' + myUser.user.display_name);
}

// 事業所情報を一覧で表示する

function showCompanies() {

    let response = UrlFetchApp.fetch('https://api.freee.co.jp/api/1/companies', header())
    let myCompanies = JSON.parse(response);
    let cids = '';
    myCompanies.companies.forEach ( items=>{
        cids += items.display_name;
        cids += ' : ';
        cids += items.id;
        cids += '\\n'; 
    })
    Browser.msgBox('認可中のユーザーが操作できる事業所：\\n' + cids);
}

// 証憑データをGoogle Driveの指定フォルダに保存する

function getReceipts() {

    let requestUrl = 'https://api.freee.co.jp/api/1/receipts?company_id=' + Company_ID + '&start_date=' + Start_Date + '&end_date=' + End_Date + '&limit=' + Limit;

    let response = UrlFetchApp.fetch(requestUrl, header());
    let myData = JSON.parse(response)

    myData.receipts.forEach(receipts =>{

        let url = 'https://api.freee.co.jp/api/1/receipts/' + receipts.id + '/download?company_id=' + Company_ID;

        // ファイル名取得

        let fileName = receipts.id;

        // 画像データを取得

        let data = UrlFetchApp.fetch(url, header());
        let fileBlob = data.getBlob().setName(fileName);

        // 取得した画像をGoogle Driveにアップロード

        let file = DriveApp.getFolderById(FolderID).createFile(fileBlob);
    })

}

function logout() {
    getDriveService().reset();
    const mes = 'freeeアプリからログアウトしました。';
    const logoutTitle = 'ログアウト終了';
    createModelessDialog(mes, logoutTitle);
}

```


## 証憑を保存したいフォルダを作成し、フォルダIDを取得

Google ドライブに、証憑を保存するためのフォルダを作成します。
フォルダには一意の「ID」文字列があります。ブラウザのアドレス欄から「folders/」に続く文字列を確認し、控えます。

![folderid.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83767/017676ff-b322-f2f4-e786-4301b61dc9bc.png)


## スプレッドシートに、必要な情報を記述する

スプレッドシートに、必要な情報を記述します。
今回のサンプルでは、シートのB列から情報を取得するように設定しています。

B1から順に

- freee アプリストアで作成したアプリの Client ID
- freee アプリストアで作成したアプリの Client Secret
- 証憑をダウンロードしたい事業所のID
- 証憑の取得期間の開始日
- 証憑の取得期間の終了日
- 取得件数
- 証憑を保存する Google Drive のフォルダID

を半角英数字で入力していきます。以下は記述例です。

![スクリーンショット 2021-07-20 16.03.27.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83767/ad7899f1-17bf-4a8f-cbcf-122a37935cf9.png)



- 認可後、保存実行

設定が全て終了したら、スプレッドシートの拡張メニューからOAuthの認可処理を行い、事業所IDを確認して、証憑を保存します。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83767/abe05265-68d5-3c3a-078e-aed5031cc141.png" width="480">


## 免責

本スクリプトは十分なテストを行っていないため、誤動作があるかもしれません。利用の際はあらかじめご了承ください。

一気に大量のデータを取得しようとすると、サーバにもスクリプトにも予期せぬ負荷がかかる可能性があります。十分にご注意ください。