[GAS][会計freee]100件以上の取引を取得しよう

freee

どうも。つじけ(tsujikenzo)です。このシリーズでは 「会計freeeで100件以上の取引を取得しよう」 ということで全3回でお送りします。今日は1回目です。

新しいシリーズのようですが、先日の「会計freeeAPIを使って残高確認表を作成しよう」 の続きになっています。

先日の記事は、かなりバタバタしてしまったので、説明が足りなかった点の補足も加えながらお届けしたいと思います。

前回の連載おさらい

会計freeeの「取引(収入/支出)」では、1レコード=1取引というレコードと、 1レコードに複数の取引がぶら下がっているレコード があります。

また、取引の中には、[payments]プロパティがないものも含まれており。 レコードによってフィールド数が違う という状況が発生してしまっています。

レコードによってフィールドの数が違うということは、 データの一括処理ができなくなり、管理が大変 です。

前回の連載では、「いろいろな書き方があると思いますが、」という一言で片付けてしまいましたので、こちらを詳しく解説したいと思います。

[GAS][会計freee]取引(収入/支出)を取得しよう
どうも。つじけ(tsujikenzo)です。この連載では 「会計freeeAPIを使って残高確認表を作成しよう」 について全7回でお送りします。今日は5回目で 「取引を取得(収入/支出)しよう」 をお届けします。前回のおさらい前回は、「事業

1回目のタイトルは 「複雑な処理は関数を切り分けよう」 です。

アジェンダ

  1. 関数の切り分け方
  2. Try…Catch構文
  3. スプレッド構文とmapメソッド

関数の切り分け方

構造化データ

今回、必要な要件定義のイメージは、下記の通りです。
– 要素に配列が含まれているレコードがある
– 配列の要素数が合わないレコードがある

このような元のデータは、まず要素数を合わせる必要があります。要素数が揃っているデータを 「構造化データ」 と呼びます。

構造化データは、人間にとって目で見辛いかもしれませんが、パソコンにとっては処理がしやすい 構造です。

//元の配列
[
['a','b',[1,2,3]],
['a',[4,5,6]],
['a','b',[7]]
]

//加工後の配列=構造化データ
[
['a','b',1],
['a','b',2],
['a','b',3],
['a','',4],
['a','',5],
['a','',6],
['a','b',7]
]

プログラミングをするさいは、構造化データを意識して、元データを加工する習慣を付けましょう。(構造化データの詳しい解説は、データベースの考察のときにお届けしようと思っています)

ミニマムをとらえる

上記のような[元の配列]を、一度に[加工後の配列]に処理しようとするとハードルが高いです。課題は小さく切り分けて、やっつけていきましょう。

今回の場合だと、1レコードに対して、下記の処理を行います。
1. 配列の要素数が合わない場合は、要素数を合わせる
2. 配列の要素に配列が入っている場合は、中の配列の要素の数だけレコードを分割する

1レコードに対して処理を行った結果、新しい配列を生成します。

そして、レコードの数だけ反復処理を行います。

複数行のレコードを処理しなければならない場合は、「まず1行だとどう処理するのか?」のように、ミニマム(処理をする最小単位)でとらえるようにしましょう。

関数化のメリットと関数リテラル

何度も繰り返す処理は、関数化したほうがよいでしょう。

関数化することで、さまざまなメリットを得ることができます。
– コードが読みやすくなる
– 注意を払うべき処理に集中できる
– メンテナンス性が高くなる
– 他のスクリプトでも使える

特に、「注意を払うべき処理に集中できる」という意味では、2~3行の処理でも、あえて関数化して、関数名をつけた方が管理しやすいこともあります。

2~3行の処理は関数から外に出さずに、同じ関数内に 関数リテラル として関数化するパターンもあります。

/**
* Dateをプロパティストアにセットする関数
*
* @param {object} Dateオブジェクト
* @return  none
*/
const setDateToPropertyStore = (date) => {
const dateFormat = Utilities.formatDate(date, "Asia/Tokyo", "yyyy/MM/dd");
const properties = PropertiesService.getScriptProperties();
properties.setProperties({"date":dateFormat});
}

たった数行の処理ですが、関数化することで、本来注意をはらうべき制御構文(例はif文)に集中でき、コードが読みやすくなります。

if(date){
setDateToPropertyStore(date);
}else{
console.log(`dateは空っぽです`);
}

Try…Catch構文

例外が発生したばあい、処理を続けるか、条件分岐ができます。

try {
//tryする処理
}
catch (エラー内容) {
//例外が発生したときの処理
}
[finally {
//例外が発生しようがしまいが最終的に行う処理
}]

実際のコードでは、レコードに[payments]プロパティがないばあい、例外が発生するため、try…catch構文で、paymentsArrayを作る処理を行っています。

let paymentsArray;
try {
paymentsArray = payments.map(payment => Object.values(payment)).flat();
} catch{
upperProperties.push(''); //配列の要素数を合わせる為
paymentsArray = [0, 0, 0, 0, 0];
}

スプレッド構文とmapメソッド

スプレッド構文

配列を結合するには「スプレッド構文」が便利です。配列名の前に …(ドット3つ) をつけると、配列の要素を展開する機能です。

「配列の要素を展開する」というのは、コード例を見ると理解が早いかもしれません。

const numbers = [1,2,3];
const strings = ['a','b','c'];

const combinedArray = [numbers, strings];
cconsole.log(combinedArray); //[[1,2,3],['a','b','c']]

const spreadArray = [...numbers, ...strings];
cconsole.log(combinedArray); //[1,2,3,'a','b','c']

mapメソッド

Arrayの組み込みオブジェクトであるmapメソッドは、反復メソッドとも呼ばれます。

配列の1つ1つの要素に対して処理を行った、「新しい配列」を生成するメソッドです。

以下のように、元になる配列名を複数形 にして、mapメソッドの引数に単数形 を使うと、コードがとても読みやすくなります。

const persons = ['Tom','Bob','John'];
const newPersons = persons.map(person => {
return `${person}様`;
}
);

console.log(newPersons); //['Tom様','Bob様','John様']

実際のコードでは、[details]プロパティの要素数だけ、新しいレコードを生成する役割を果たしています。

//detailsプロパティを基点に、fullArrayを整形
const fullArray = detailsArray.map(detail => {

return [...upperProperties, ...detail, ...paymentsArray];

}
);

前回の連載で、「このような関数を作成しました。」と一言で片付けてしまった関数はこちらです。

/**
* 1つの取引(収入/支出)オブジェクトを渡すと、全ての2次元配列を返す関数
*
* @param {object}  1つの取引(収入/支出)オブジェクト
* @return {object} 2次元配列
* //detailsプロパティを格納のマジックナンバーはfreeeでプロパティの増減仕様変更があると動かなくなる
*/
function getFullArray_(deal) {

//最上層のプロパティを格納
const upperProperties = Object.values(deal);

//detailsプロパティを格納
const details = deal['details'];
const detailsArray = details.map(detail => Object.values(detail));

//paymentプロパティを格納
const payments = deal['payments'];

//deal['payments']プロパティが無い場合、paymentsArrayを作る
let paymentsArray;
try {
paymentsArray = payments.map(payment => Object.values(payment)).flat();
} catch(e){
upperProperties.push(''); //配列の要素数を合わせる為
paymentsArray = [0, 0, 0, 0, 0];
Logger.log(e);
}

//detailsプロパティを基点に、fullArrayを整形
const fullArray = detailsArray.map(detail => {

return [...upperProperties, ...detail, ...paymentsArray];

}
);

return fullArray;

}

この関数は1レコードを渡すと、レコードを分析して、新しいレコード群を生成します。

まさに、この画像でお伝えした関数でした。

まとめ

以上で、「会計freeeで100件以上の取引を取得しよう」 の1回目として 「複雑な処理は関数を切り分けよう」 をお届けしました。

次回は、「レコードの開始値(オフセット値)を操作しよう」 をお届けします。

このシリーズの目次

[GAS][会計freee]100件以上の取引を取得しよう
1. [GAS][会計freee]複雑な処理は関数を切り分けよう
2. [GAS][会計freee]レコードの開始値(オフセット値)を操作しよう
3. [GAS][会計freee]スプレッドシートへ出力する関数

どうも。つじけ(tsujikenzo)です。このシリーズでは 「会計freeeで100件以上の取引を取得しよう」 ということで全3回でお送りします。今日は2回目です。

前回のおさらい

前回は、「会計freeeで100件以上の取引を取得しよう」 の1回目として 「複雑な処理は関数を切り分けよう」 をお届けしました。

[GAS][会計freee]100件以上の取引を取得しよう
どうも。つじけ(tsujikenzo)です。このシリーズでは 「会計freeeで100件以上の取引を取得しよう」 ということで全3回でお送りします。今日は1回目です。新しいシリーズのようですが、先日の「会計freeeAPIを使って残高確認表

今回は、「レコードの開始値(オフセット値)を操作しよう」 をお届けします。

アジェンダ

  1. リファレンスとリクエストURL
  2. whileを使った可変offset
  3. tryGetRecord_関数

リファレンスとリクエストURL

会計freeeから取引を取得するさい、異なる取引先ごとに都度サーバーへアクセスすると、その分サーバーに負担をかけることになります。

できれば、取得した取引をGASやスプレッド上で振り分けるなど、サーバーへのアクセスは1度だけにしたいものです。

その心構えは素晴らしいのですが、会計freeeのAPIリファレンスを確認すると、「一度に取得できる最大取引数は100件」とあります。

自動で全件取得できるように、処理を自動化してみましょう。

開始日と終了日

リクエストURLのパラメータには、[開始日]と[終了日]プロパティがあります。日時の範囲を指定してレコードを取得できます。

limit

デフォルトでは、取得するレコードのlimitは20件ですが、最大[100]を指定できます。

const requestUrl = `https://api.freee.co.jp/api/1/deals?company_id={事業所ID}&start_issue_date=${startIssueDate}&end_issue_date=${endIssueDate}&limit=100`;

offset

リクエストURLのパラメータには、[offset(オフセット)]という、 取得レコードの開始値プロパティ があります。

100件以上のレコードがある場合は、0~99、100~199、200~299のように、100件ずつ取得できそうです。※offsetは0(ゼロ)始まりに注意が必要です。

総レコード数の取得

取引(deals)のエンドポイントでは、最終取得レコードに到達すると、総レコード数を含む [meta]プロパティ を発行します。※1

[meta]プロパティには、[total_count]プロパティ が含まれており、整数で、総レコード数を格納しています。

(※1 個人的な憶測です。違っていたら公式さんごめんなさい。)

「期間範囲」「最大取得レコード数」「オフセット値」という3つの素材が揃いましたので、Loopを回してみましょう。

whileを使った可変offset

「期間範囲」は固定でいいと思います。全件取得なので、1年分でも構いませんし、1か月分でも構いません。

//リクエストURLに必要な開始日と終了日
const startIssueDate = '2021-01-01';
const endIssueDate = '2021-12-31';

「総レコード数」を取得するために、別途、1件のみレコードを取得して、総レコード数を変数に格納しておきます。

//総レコード数を確認するためのリクエストURL(limit=1がポイント)
const requestUrl = `https://api.freee.co.jp/api/1/deals?company_id=2606452&start_issue_date=${startIssueDate}&end_issue_date=${endIssueDate}&limit=1`;

//HTTPリクエストをリクエストURLに送信する
const response = UrlFetchApp.fetch(requestUrl, params);

//JSONオブジェクトに変換
const jsonObj = JSON.parse(response);

//総レコード数
const totalCount = jsonObj['meta']['total_count']; //例:2456

offset値を準備して、while文で回します。

先に完成したコードをお伝えします。

let offset = 0;

//全レコードを取得する
while (offset < totalCount) {

    const requestUrl = `https://api.freee.co.jp/api/1/deals?company_id=2606452&start_issue_date=${startIssueDate}&end_issue_date=${endIssueDate}&limit=100&offset=${offset}`;

    //HTTPリクエストをリクエストURLに送信する
    const response = UrlFetchApp.fetch(requestUrl, params);

    //スプレッドシートへ出力
    setAllDeals_(response);

    //一度に取得できるlimitが100件なのでoffsetも100ずつcountUpする
    offset += 100;

    //3秒待機
    Utilities.sleep(3000);

  }

  console.log('レコードはこれ以上ありません');

while文は 条件式がtrueであり続ける限りLoop処理を行う 構文です。

offset値が総レコード数を越えるまで、Loopを行います。

const totalCount = 2456;
let offset = 0;
while(offset < totalCount){
  //処理
  offset += 100;
}

スプレッドシートへの出力は、次回お届けします。

setAllDeals_(response);

まとめ

以上で、「レコードの開始値(オフセット値)を操作しよう」 ということで「期間範囲」「最大取得レコード数」「オフセット値」をお届けしました。

特に、[limit]プロパティを1にして、総レコード数を含む [meta]プロパティ[total_count]プロパティ を取得したのは、特徴的だったかと思います。

次回は、「スプレッドシートへ出力する関数」 をお届けします。

このシリーズの目次

[GAS][会計freee]100件以上の取引を取得しよう
1. [GAS][会計freee]複雑な処理は関数を切り分けよう
2. [GAS][会計freee]レコードの開始値(オフセット値)を操作しよう
3. [GAS][会計freee]スプレッドシートへ出力する関数

どうも。つじけ(tsujikenzo)です。このシリーズでは 「会計freeeで100件以上の取引を取得しよう」 ということで全3回でお送りします。今日は3回目です。

前回のおさらい

前回は、「レコードの開始値(オフセット値)を操作しよう」 ということで「期間範囲」「最大取得レコード数」「オフセット値」をお届けしました。

[GAS][会計freee]レコードの開始値(オフセット値)を操作しよう
どうも。つじけ(tsujikenzo)です。このシリーズでは 「会計freeeで100件以上の取引を取得しよう」 ということで全3回でお送りします。今日は2回目です。前回のおさらい前回は、「会計freeeで100件以上の取引を取得しよう」

今回は、最終回で 「スプレッドシートへ出力する関数」 をお届けします。

アジェンダ

  1. レスポンスの受け取りと2次元配列化
  2. 見出し行の設定
  3. スプレッドシートへの出力と重複削除

レスポンスの受け取りと2次元配列化

スプレッドシートへの出力は、関数を分けました。まず、引数にレスポンスを渡します。

setAllDeals_(response);

受け取ったレスポンスをJSONオブジェクトに変換し、1行ずつレコードを分解するサブ関数getFullArray_()を使って2次元配列を整形します。

//JSONオブジェクトに変換
const jsonObj = JSON.parse(response);

//取引(収入/支出)の配列を取得する
const deals = jsonObj['deals'];

//全ての項目を2次元配列化する(サブ関数を使用)
const values = deals.map(deal => getFullArray_(deal)).flat();

見出し行の設定

どんなデータにも、見出し行となる1行目は必要だと思います。セルの中身が何を示しているのか、分かりやすく管理するためです。

あらかじめ、シートの1行目に手で入力しておいて、2行目以降にデータを流し込むことが多いと思います。

しかし、列数が変更になったり順番が変わったりすると、管理が大変です。できれば 取得したレコードから見出し行を作成したい ものです。

注意が必要な点は、プロパティを見出し行に使用するさいは、プロパティだけでは、名前が重複してしまう点です。

なので、[details]と[payments]については、見出しにそれぞれのプロパティを付与します。

見出し行の生成

[deals直下]、[detailsプロパティ]、[paymentsプロパティ]の3種類なので、ベタ書きしました。

//見出しを先頭に追加
const upperPropertyKeys = Object.keys(deals[0]);

const detailPropertyKeys = Object.keys(deals[0]['details'][0]);
const detailKeys = detailPropertyKeys.map(detailPropertyKey => 'details_' + detailPropertyKey);

const paymentPropertyKeys = Object.keys(deals[0]['payments'][0]);
const paymentKeys = paymentPropertyKeys.map(paymentPropertyKey => 'payments_' + paymentPropertyKey);

最後はスプレッド構文で、headersという2次元配列を作成したものを、valuesの先頭行に足しています。

const headers = [...upperPropertyKeys, ...detailKeys, ...paymentKeys];
values.unshift(headers);

繰り返し処理になるので、必要に応じて、関数化した方がいいかもしれませんね。

スプレッドシートへの出力と重複削除

スプレッドシートへ出力をするときは、まず、貼り付け位置の基点となるセル位置を、getLastRow()メソッドで取得します。

そして下記のように、常に 最終行の次の行に貼り付ける ようにします。

//スプレッドシートシートへ出力
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('シート1');

const lastRowNext = sheet.getLastRow() + 1;
sheet.getRange(lastRowNext, 1, values.length, values[0].length).setValues(values);

valuesには、見出し行も含まれていますので、毎回、見出し行も貼り付けてしまいますが、レコードの重複削除 をしますので問題ありません。

重複削除

重複削除は、スプレッドシートのメソッドを使う のが最もコスパがいいと思います。(「2次元配列の段階で重複削除する」「集合論に基づいてCRUD操作に準拠する」など、さまざまな考察をした後の個人的な感想です)

スプレッドシートの重複削除メソッドは2種類しかありません。2種類を見比べると、どのようなロジックでレコードの重複を判定(および削除)しているのか伝わると思います。

//引数無し。行全体を比較する
removeDuplicates()

//使用例
range.removeDuplicates()

//引数に、重複を検討するインデックスを指定する
removeDuplicates(columnsToCompare)

//使用例
range.removeDuplicates([2,4])

完成系

すべてをつなげた完成系のコードはこちらです。

/**
* 取引(収入/支出)一覧をスプレッドシートへ出力する関数
*
* @param {object} レスポンス
* @return none
*/
function setAllDeals_(response) {

//レスポンスをJSONオブジェクトに変換
const jsonObj = JSON.parse(response);

//取引(収入/支出)の配列を取得する
const deals = jsonObj['deals'];

//全ての項目を2次元配列化する(サブ関数を使用)
const values = deals.map(deal => getFullArray_(deal)).flat();

//見出しを先頭に追加
const upperPropertyKeys = Object.keys(deals[0]);

const detailPropertyKeys = Object.keys(deals[0]['details'][0]);
const detailKeys = detailPropertyKeys.map(detailPropertyKey => 'details_' + detailPropertyKey);

const paymentPropertyKeys = Object.keys(deals[0]['payments'][0]);
const paymentKeys = paymentPropertyKeys.map(paymentPropertyKey => 'payments_' + paymentPropertyKey);

const headers = [...upperPropertyKeys, ...detailKeys, ...paymentKeys];
values.unshift(headers);

//スプレッドシートシートへ出力
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('シート1');

const lastRowNext = sheet.getLastRow() + 1;
sheet.getRange(lastRowNext, 1, values.length, values[0].length).setValues(values);

//重複削除
sheet.getDataRange().removeDuplicates();
}

まとめ

以上で、「スプレッドシートへ出力する関数」 をお届けしました。

これにて、会計freeeから必要な取引を取得する準備が、ある程度できたのではないでしょうか。

取引の登録については、簿記の知識も必要になり、少しハードルが高くなります。また気付いたことがあれば記事を書きたいと思います。

個人的に簿記資格を取る予定は今のところありません。笑

このシリーズの目次

[GAS][会計freee]100件以上の取引を取得しよう
1. [GAS][会計freee]複雑な処理は関数を切り分けよう
2. [GAS][会計freee]レコードの開始値(オフセット値)を操作しよう
3. [GAS][会計freee]スプレッドシートへ出力する関数

Comments

Copied title and URL