クッキング

GAS x freeeAPIライブラリのトリセツ「自動同期で取得した取引に自動でタグを付与しよう」その3 – 取引を加工してPUT(更新)しよう

自動同期して取得した取引に各種のタグを自動で付与しようという挑戦の最終回です。

前回の記事はこちら。

前回までで更新対象取引の絞り込みとバックアップまで完成しています。ここからいよいよ更新作業に入ります。

今回は自動同期で取得したBASEの取引に以下の3つのタグを付与していきたいと思います。

  • 取引先:BASE
  • 部門:EC
  • メモタグ:*GAS更新

運用上のメインの目的は、取引先と部門タグを付与することですが、 今回のようにプログラミングを使って更新した取引を、後でfreeeのUI上でソートできるようにメモタグを付与しておくと便利です。

完成したスクリプトはこちら。

function renewBaseDeals03() {

  /* GAS x freeeAPIライブラリの操作オブジェクトを取得する */
  const accessToken = getService().getAccessToken(); // アクセストークンを取得
  const company_id = Number(ScriptProperties.getProperty('COMPANY_ID')); // 事業所IDはプロパティストアに格納
  const deals_freeeAPI = freeeAPI.deals(accessToken, company_id); // freeeAPIライブラリのdeals操作オブジェクトを作成

  /* 前月初日からの取引に絞り込んで取得する */
  const today = new Date(); // 今日のDateオブジェクト
  const beginLastMonth = new Date(today.getFullYear(), today.getMonth() - 1, 1); // 前月初日のDateオブジェクト
  const start_issue_date = Utilities.formatDate(beginLastMonth, 'JST', 'yyyy-MM-dd'); // yyyy-MM-ddの文字列に変換
  deals_freeeAPI.queries.start_issue_date = start_issue_date; // APIリクエストのパラメータの発生日に前月初日を代入
  const allDeals = deals_freeeAPI.getAllDeals(); // 条件に合致する取引オブジェクトを全て取得する

  /* 決済口座:BASE(ECサイト)・取引先:未選択 に該当するオブジェクトを抽出する */
  const trgWalletableId = freeeAPI.walletables(accessToken, company_id).getIdByName('BASE(ECサイト)'); // 口座名から口座IDを取得
  const trgDeals = allDeals.filter(deal => deal.partner_id === null && Array.isArray(deal.payments) && deal.payments.some(payment => payment.from_walletable_id === trgWalletableId));

  /* 更新対象IDの取引をバックアップの上で更新 */
  const timeStamp = Utilities.formatDate(new Date(), 'JST', 'yyyyMMddhhmmss'); // バックアップ用のファイルの名前に使用するタイムスタンプ
  const fileName = `${company_id}_deals_${timeStamp}.txt`; // 事業所ID_操作エンドポイント_タイムスタンプ.txtのフォーマットでバックアップを保存
  const folderId = ScriptProperties.getProperty('ID_BACKUP_FOLDER'); // バックアップファイルを保存するグーグルドライブのフォルダIDを指定
  DriveApp.getFolderById(folderId).createFile(fileName, JSON.stringify(trgDeals));

  /* 上書きしたいプロパティを定義しておく */
  const partner_id = freeeAPI.partners(accessToken, company_id).getIdByName('BASE');
  const section_id = freeeAPI.sections(accessToken, company_id).getIdByName('EC');
  const tag_id = freeeAPI.sections(accessToken, company_id).getIdByName('*GAS更新');

  /* 更新対象の各オブジェクトに対してプロパティを上書きしPUT(更新)していく */
  trgDeals.forEach(objDeal => {
    const deal_id = objDeal.id;
    const deal_freeeAPI = freeeAPI.deal(accessToken, company_id); // freeeAPIライブラリのdeal操作オブジェクトを作成

    const keysKeep = Object.keys(deal_freeeAPI.objPut); // PUT用に残しておくべき第1階層のキー配列
    const objDealNew = deleteDiffProperties(objDeal, keysKeep); // PUTに不要なキーの削除
    objDealNew.partner_id = partner_id; // 取引先IDを代入

    const keysKeepDetails = Object.keys(deal_freeeAPI.objPut.details[0]); // PUT用に残しておくべき第2階層(details以下)のキー配列
    objDealNew.details = objDealNew.details.map(detail => {
      if (objDealNew.type === 'income' && detail.entry_side === 'debit') { detail.amount = detail.amount * -1, detail.vat = detail.vat * -1 };
      if (objDealNew.type === 'expense' && detail.entry_side === 'credit') { detail.amount = detail.amount * -1, detail.vat = detail.vat * -1 };
      detail.section_id = section_id; // 部門IDを付与
      detail.tag_ids = [tag_id]; // メモタグIDを付与
      const detailNew = deleteDiffProperties(detail, keysKeepDetails); // PUTに不要なキーの削除
      return detailNew;
    });
    deleteBlankProperties(objDealNew); // 値がnullや空文字列などのプロパティを削除
    deal_freeeAPI.putDeal(deal_id, objDealNew) // 取引を更新
  });
}

上書きしたい項目(プロパティ)を定義しておく

freee APIでのタグの更新にはそれぞれのシステムIDが必要になります。

  • partners
  • sections
  • tags

という各エンドポイント操作オブジェクトにはgetIdByName()メソッドを用意していますので、freeeに登録済のタグであれば表示名から取得できます。

const partner_id = freeeAPI.partners(accessToken, company_id).getIdByName('BASE');
const section_id = freeeAPI.sections(accessToken, company_id).getIdByName('EC');
const tag_id = freeeAPI.sections(accessToken, company_id).getIdByName('*GAS更新');

更新対象の各オブジェクトに対してプロパティを上書きしPUT(更新)していく

いよいよメインディッシュです。更新対象の各オブジェクトに対してプロパティを上書きしPUT(更新)していく処理を実装します。

定数trgDealsに代入された配列の要素として更新したい各取引のオブジェクトが格納されています。この各取引に対して

取引先・部門・メモタグのプロパティ上書き → freee APIでPUTリクエスト

という処理を繰り返し行いたいためArrayオブジェクトに使用できるforEach() メソッドを使用しています。

trgDeals.forEach(objDeal => {各要素を抜き出したobjDealを用いて繰り返し行いたい処理});

取引IDの取り出しとfreeeAPIライブラリ deal操作オブジェクトを生成する

freee APIのPUT(更新)の処理は1つ1つの取引に対して実行しなければならないため、まず取引のIDを取得します。またGAS x freeeAPIライブラリでは1つ1つの取引に対して操作する場合はdelasクラスとは別にdealクラスを用意しています。

取引の更新や登録は、このdealクラスに実装していますので、このdeal操作オブジェクトをまず生成します。

const deal_id = objDeal.id;
const deal_freeeAPI = freeeAPI.deal(accessToken, company_id);

PUT(更新)に不要なキーを削除したい

以下の記事でも紹介していますが、freee APIのGETリクエストで取得した取引オブジェクトには、POST(登録)やPUT(更新)時には不要なプロパティが含まれています。

この不要なプロパティを削除するために以下の記事では、deleteDiffProperties関数を作成しました。これは、残しておきたいキー一覧に含まれないプロパティをオブジェクトから削除する関数で、以下の記事では、オブジェクト内のオブジェクトなどネスト構造にも対応する再帰関数として作成しました。

今回のPUTのリクエストでも同様に不要なプロパティを削除していきたいのですが、実は今回のケースだと再帰関数だと不具合が出てしまいました。これはfreee APIのオブジェクトのキーが複数階層で同じ名前のものがある(例:id)ためで、消してほしいところで消してくれなかったりと再帰関数だとうまくいきません。

ということで、deleteDiffProperties関数は再帰関数ではなく、1階層にのみ有効な関数として書き換えます。

  /**
   * オブジェクトからテンプレートのキー一覧との共通しないプロパティを削除する関数
   * @param  {Object}  obj - 元となるオブジェクト
   * @param  {Array}   keysKeep - 残したいプロパティの一覧
   * @return  {Object}  obj - 共通しないキーのプロパティを削除したオブジェクト
   */

function deleteDiffProperties(obj, keysKeep) {
    const keys = Object.keys(obj);
    // テンプレートのキー一覧との共通しないキーの一覧を配列で取得
    const diffKeys = keys.filter(key => !keysKeep.includes(key));
    // 共通しないキーのプロパティを削除
    diffKeys.forEach(difKey => delete obj[difKey]);
    return obj;
  }

第1階層の不要なプロパティを削除し取引先IDを代入する

PUTリクエストに必要な残しておくべきキーの一覧は、deal操作オブジェクト内に定義しているPUT用のサンプルオブジェクトから借りてきます。

// PUT用に残しておくべき第1階層のキー配列
const keysKeep = Object.keys(deal_freeeAPI.objPut); 

// PUTに不要なキーの削除
const objDealNew = deleteDiffProperties(objDeal, keysKeep); 

定数objDealNewにはPUTに不要なキーを削除した取引オブジェクトとなっています。この第1階層には、取引先を識別するpartner_idプロパティが存在しますので、今回付与したい取引先IDをこのプロパティの値に代入します。

objDealNew.partner_id = partner_id; // 取引先IDを代入

第2階層の取引明細オブジェクトも不要なプロパティを削除し部門IDとメモタグIDを代入する

第1階層同様にdetailsプロパティ(配列)に格納されている取引明細行のオブジェクトに関しても不要なプロパティの削除と部門IDとメモタグIDの代入を行います。

detailsプロパティの配列の各要素に対して更新(代入)処理を実行し、その結果を反映させた新たなdetailsプロパティを用意したいのでArrayオブジェクトに使用できるmap()メソッドを用いています。

// PUT用に残しておくべき第2階層(details以下)のキー配列
const keysKeepDetails = Object.keys(deal_freeeAPI.objPut.details[0]); 
objDealNew.details = objDealNew.details.map(detail => {各明細行への処理を行い、戻り値として新しい明細オブジェクトを返す});

map()メソッド内の反復処理のなかで部門IDとメモタグIDを代入しています。

detail.section_id = section_id; // 部門IDを付与
detail.tag_ids = [tag_id]; // メモタグIDを付与

freee APIでの取引の登録・更新時のトラップ「貸借」

この反復処理のなかで

if (objDealNew.type === 'income' && detail.entry_side === 'debit') { detail.amount = detail.amount * -1, detail.vat = detail.vat * -1 };
if (objDealNew.type === 'expense' && detail.entry_side === 'credit') { detail.amount = detail.amount * -1, detail.vat = detail.vat * -1 };

という不思議な処理をしています。

これは、GETで取得したfreeeの取引オブジェクトの明細オブジェクトには、entry_sideというプロパティがあって、ここで貸借(貸方: credit, 借方: debit)の情報を保持しているのですが、例えばマイナスの金額の場合はこの貸借の項目が逆になっていることで表現されています。

PUT更新時に金額は変えないでおこうとamountのプロパティをそのままにしていても貸借の情報を参考に金額のプラス・マイナスを表現しないと金額が変わってしまうんですね。

ということで

  1. 収入取引かつ明細貸借:借方
  2. 支出取引かつ明細貸借:貸方

の明細の金額と消費税額に関してはマイナスとなるように-1をかけています。

仕上げに明細行も不要なプロパティを削除しよう

今回、deleteDiffProperties関数を再帰関数にすることを放棄したので、各明細オブジェクトの不要なプロパティも削除していく必要があります。

// PUTに不要なキーの削除
const detailNew = deleteDiffProperties(detail, keysKeepDetails); 

これで明細行の更新が完了しました。

return detailNew;

オブジェクトから値がnullやundefined、空文字列のプロパティを削除する

最後の仕上げです。以下の記事のPOST(登録)処理同様にPUTリクエストを送るオブジェクトからオブジェクトから値がnullやundefined、空文字列のプロパティがあるとエラーが出るため、これらを削除します。

こちらは再帰関数のままでOKです。

/**
 * オブジェクトから値がnullやundefined、空文字列のプロパティを削除する関数
 * @param  {Object}  obj - 元となるオブジェクト
 * @return  {Object}  値がnullや空のプロパティを削除したオブジェクト
 */

function deleteBlankProperties(obj) {
  Object.keys(obj).forEach(key => {
    const value = obj[key]
    if (typeof value === 'object' && value !== null && !Array.isArray(value)) { deleteBlankProperties(value) }; // nullでないオブジェクト
    if (typeof value === 'object' && Array.isArray(value) && value.length > 0) { deleteBlankProperties(value) }; // 要素が1以上の配列
    if (typeof value === 'object' && value !== null && !Array.isArray(value) && Object.keys(value).length === 0) { delete obj[key] }; // nullでない空のオブジェクト
    if (typeof value === 'object' && Array.isArray(value) && value.length === 0) { delete obj[key] }; // 要素が空の配列
    if (value === null) { delete obj[key] };
    if (value === undefined) { delete obj[key] };
    if (value === '') { delete obj[key] };
  });
  return obj;
}

ということで更新用のobjDealNewからプロパティを削除します。

// 値がnullやundefined、空文字列などのプロパティを削除
deleteBlankProperties(objDealNew);

GAS x freeeAPIライブラリで取引の更新のリクエストは1行でOK

長らくお疲れ様でした。加工が終わった更新用のオブジェクトができてしまえば、GAS x freeeAPIライブラリを使って1行で更新リクエストが送れます。

deal_freeeAPI.putDeal(deal_id, objDealNew) // 取引を更新

これは、freeeAPIライブラリ deal操作オブジェクトのputDeal(deal_id, payload) – JSONオブジェクトから取引を更新するメソッドで実現できます。

おわりに

「自動同期で取得した取引に自動でタグを付与しよう」を全3回でお送りしました。このスクリプトをトリガーで定期実行させることで

  • 決済口座:BASE(ECサイト)
  • 取引先:未選択
  • 発生日:前月初日以降

の取引に

取引先:BASE
部門:EC
メモタグ:*GAS更新

のタグを自動で付与できるようになりました。

ここまででお気づきかもしれませんが、この取引自動更新は税区分の変更や品目タグの付与などにも応用できます。

この場合は取引の明細オブジェクトにdescriptionというプロパティがあり、このプロパティの値に「決済手数料」や「送料」などの文言が含まれる文字列があります。この文字列を判定して、とある規則性に従って税区分を変更したり、品目を付与したりが可能です。

シリーズ目次

  1. GAS x freeeAPIライブラリのトリセツ「自動同期で取得した取引に自動でタグを付与しよう」その1 – 条件で指定した更新対象のfreee取引のみ取得する
  2. GAS x freeeAPIライブラリのトリセツ「自動同期で取得した取引に自動でタグを付与しよう」その2 – 更新前に取引データをバックアップしよう
  3. GAS x freeeAPIライブラリのトリセツ「自動同期で取得した取引に自動でタグを付与しよう」その3 – 取引を加工してPUT(更新)しよう

Amazon欲しい物リスト公開しています。

開発者のモチベーションアップのためにAmazon欲しい物リストを公開しております。役に立ったよ!という方の感謝の気持ちで何かいただけるのであれば嬉しいです笑

Amazon欲しい物リスト

タグ: , ,
Share on:
Previous Post
時計
GAS活用法

GASで前回のスクリプト実行日時から今回の実行までの期間をどう指定するか

Next Post
ディスクドライブ
freeeAPI

GAS x freeeAPIライブラリのトリセツ「自動同期で取得した取引に自動でタグを付与しよう」その2 – 更新前に取引データをバックアップしよう