kintoneデータをスプレッドシートに自動抽出して、集計をスピーディにしました

きっかけ

kintoneカスタマイズスペシャリストの勉強(JavaScript)を始めて、APIトークンとかJSONの話が出てきました。 社内でkintoneとGASでAPI連携してデータの自取取得を試みている部署がるのを思い出して「そういえばこの前やり方聞かれたけど答えられなかったな。もし自動取得できたら自分の業務でもかなり時短できるぞ。ちょっと調べてみよう」と思ったのがきっかけです

効率化の必要性

昨年から社内ではSFAを入れ替えており、一定の進捗確認の仕組みはすでにありました。今年は「このレイアウトで数字を見たい」「商品別、販売経路別、●●別で見たい。あと時期の区切りは~~で」と様々な切り口から売上のトレンドを、しかもなるべくリアルタイムで知りたい。という要望も多くなりました。データはありますが、全部すぐに出せるほどは整ってないんですね。

対応するには、kintoneからcsv出力→スプレッドシートに張り付け→関数使って集計。が早いです。 早いんですが、複数の要望を定期的に集計。となるとまあまあ時間かかります。 ですので、自動化ができれば、私の負担が軽減されるだけでなく、営業組織全体の意思決定も迅速化にもつながるため、重要度も高いと考えました

API連携時のデータの抽出とスプレッドシート出力の実装手順

  1. GASを使用してkintoneからデータを500件ずつ取得
  2. 取得したデータをスプレッドシート出力用の配列変数に格納
  3. 500件ずつの処理を繰り返し、読み込み終了後にスプレッドシートに出力

※kintoneからのAPIでのレコード取得は一度に500件が限度なのでループさせます。
※今回は営業の案件のデータで、一つの案件に複数商品(テーブルデータ)が含まれます。商品が複数ある場合、商品ごとで一行ずつ出力します。テーブルデータ以外はコピーして同じデータを出力します
※一部の商品(テーブルデータ)は、あるフィールドのフラグごとに出力する列を分けます。
※値が空だと列数がずれることがあるので、空欄判定をして"-"を出力します。

実装コード(項目名をxxxにしたり、一部端折ってます)

function fetchRecords(startIndex, limitnumber) {
  const subdomain = "xxxxxxxxxx";
  var urlGetRecords = "https://" + subdomain + ".cybozu.com/k/v1/records.json";
  var appId = xx;
  var apiToken = 'xxxxxxxxxx';

//xxxにフィールドコード名を入れる
  var paramFields = "&fields=$id," + encodeURIComponent("xxx,xxx,xxx,xxx,xxx,xxx,xxx,xxx,~~~~~~");
  var paramQuery = "&query=" + encodeURIComponent("xxx in (条件) and xxx in (条件 order by $id desc limit " + limitnumber + " offset " + startIndex);
  var url = urlGetRecords + "?app=" + appId + paramFields + paramQuery;
  var res = JSON.parse(UrlFetchApp.fetch(url, { "method": "get", "headers": { "X-Cybozu-API-Token": apiToken } }));
  return res.records;
}

function convertToJapanTime(utcTimeString) {
  var utcDate = new Date(utcTimeString);
  
  // 日本時間に変換
    var japanDate = Utilities.formatDate(utcDate, "GMT+9", "yyyy/MM/dd");
  return japanDate;
}

function getNameFromRecord(record, field) {
  var data = record[field].value;
  return (data && data.name) ? data.name : "-";
}

function myFunction4() {
  const limit = 500;
  const sheetOutpust = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('データ取得');

  // ヘッダ行の生成
  var headerRow = ["ヘッダ1", "ヘッダ2"~~~~~"ヘッダ43"];
  var output = [headerRow]; // ヘッダ行を先頭に追加

  var startIndex = 0;
  var records = fetchRecords(startIndex, limit);

  while (records.length > 0) {
      records.forEach(function (record) {
          var baseRow = [];
          //ユーザー選択や組織選択のデータ抽出の書き方
          baseRow.push(record["xxx"].value && record["xxx"].value.length > 0 ? record["xxx"].value[0].name : "-");
          //日付関連のデータ抽出の書き方
          baseRow.push(convertToJapanTime(record["xxx"].value) || "-");
          //文字列、レコード番号など単純にデータ取得する場合
          baseRow.push(record["xxx"].value || "-");
          
          //更新者・作成者の取得はこんな感じ
          var updaterName = getNameFromRecord(record, "xxx");
          var creatorName = getNameFromRecord(record, "xxx");
          baseRow.push(updaterName);
          baseRow.push(creatorName);

          //他データも同様に取得
          //...
          //...

          // テーブルの情報はあるフィールドの値で場合分けして取得。テーブルデータの数だけ行を追加。テーブルデータ以外の共通の値はコピーして同じ情報を持つ。関係ないテーブルデータは"-"とする。
          if (record["xxx"].value === "フラグ1") {
              // テーブルのデータが存在する場合
              if ("xxx" in record && record["xxx"].value.length > 0) {
                  record["テーブル"].value.forEach(function (tableData) {
                    if (tableData.value["xxx"].value !== null) {
                      var row = baseRow.slice(); // ベース行をコピー 
                      row.push(tableData.value["xxx"].value);
                      row.push(tableData.value["xxx"].value);
                      row.push(tableData.value["xxx"].value);
                      row.push(tableData.value["xxx"].value);
                      row.push(record["xxx"].value);
                      row.push("-", "-", "-", "-", "-"); 
                      row.push("-", "-", "-", "-", "-"); 
                      row.push("-", "-"); 
                      output.push(row);
                    }
                  });
              } else {
                // 何もしない
              }
          }
          else if (record["xxx"].value === "フラグ2") {
            // テーブル_1のデータが存在する場合
            if ("テーブル_1" in record && record["テーブル_1"].value.length > 0) {
                record["テーブル_1"].value.forEach(function (tableData) {
                  if (tableData.value["xxx"].value !== null) {
                    var row = baseRow.slice(); // ベース行をコピー 
                    row.push("-", "-", "-", "-", "-"); 
                    row.push(tableData.value["xxx"].value);
                    row.push(tableData.value["xxx"].value);
                    row.push(tableData.value["xxx"].value);
                    row.push(tableData.value["xxx"].value);
                    row.push(record["xxx"].value);
                    row.push("-", "-", "-", "-", "-"); 
                    row.push("-", "-"); 
                    output.push(row);
                  }
                });
            } else {
              //何もしない
            }
          }
          else if (record["xxx"].value === "フラグ3") {
            // テーブル_0のデータが存在する場合
            if ("テーブル_0" in record && record["テーブル_0"].value.length > 0) {
                record["テーブル_0"].value.forEach(function (tableData) {
                  if (tableData.value["xxx"].value !== null) {
                    var row = baseRow.slice(); // ベース行をコピー 
                    row.push("-", "-", "-", "-", "-");
                    row.push("-", "-", "-", "-", "-");
                    row.push(tableData.value["xxx"].value);
                    row.push(tableData.value["xxx"].value);
                    row.push(tableData.value["xxx"].value);
                    row.push(tableData.value["xxx"].value);
                    row.push(record["xxx"].value);
                    row.push("-", "-");
                    output.push(row);
                  }
                });
            } else {
              //何もしない
            }
          }
          
          //フラグ関係なく、テーブル情報があれば別行でデータ取得
          if ("テーブル_2" in record && record["テーブル_2"].value.length > 0) {
            record["テーブル_2"].value.forEach(function (tableData) {
              if (tableData.value["xxx"].value !== null) {
                var row = baseRow.slice(); // ベース行をコピー
                row.push("-", "-", "-", "-", "-");
                row.push("-", "-", "-", "-", "-");
                row.push("-", "-", "-", "-", "-");
                row.push(tableData.value["xxx"].value);
                row.push(tableData.value["xxx"].value);
                output.push(row);
              }
            });
          } else {
            //何もしない
          }
          
    });
    startIndex += limit;
    records = fetchRecords(startIndex, limit);
  }
    // スプレッドシートに出力
    //console.log(output);
    var sheetOutput = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('データ取得');
    sheetOutput.getRange(1, 1, output.length, output[0].length).setValues(output); // ヘッダ行も含めて出力
}

挑戦したことと学び

  • 課題・問題点

    • エラー原因がわからなくてメンタル消耗
      • JSON形式の扱い方、API連携の文法など、最初は意味不明でエラー原因読んでもさっぱりだったけど、原因わからなくてもとにかく仮説立て⇔実行でトライアンドエラー。心折れそうになったら時間をおいて再度チャレンジ。わからなくてもとりあえず机の上で60分は考える!で時間使ってなんとか乗り越えました。おかげで多少なり理解できた気がします笑
  • 学び

    • API連携のデータ取得はJSON形式が多い→汎用的な知識?
      • 一般的にAPI連携で取得するデータはJSON形式が多いようです。なのでこの経験によって、kintone以外のシステムも、API連携ができるものはデータを扱えるようになったと思います。
    • ユーザー選択は配列で格納、作成者や更新者は配列ではなく直接code・nameで保存
      • これはkintoneの仕様の話ですが、データ取得のコードの書き方で「ユーザー選択フィールドも作成者フィールドも同じっしょ」と思って実行したらエラーになりました。理由としては作成者や更新者は、配列ではないのでちょっと書き方が変わります。これは、kintoneの仕様として、複数ユーザが作成者になるはずが無い。と考えてデータ構造を変えているんだなと学びました。カスタマイズスペシャリスト資格でも必要そうな知識を知れて良かった。

成果と今後の展望

  • 成果

    • 営業組織の一部門の進捗管理を自動化し、業務効率化を達成しました。営業担当者別の契約・解約・提案状況を顧客名、商品や個数・金額など約50項目を自動で出力します。
  • 今後の展望

    • 個人:カスタマイズスペシャリストの勉強を続けるとともに、JSを用いたフィールドの表示・非表示切り替えに取り組みます。
    • 業務:営業企画で管理するものも自動化していきます。BIツールの活用や、他の業務領域での効率化を目指します。

最後に

読んでくださりありがとうございます。 現状はkintoneの記事が2つのみですが、kintone以外の取り組みも書いていこうと思いますので、よろしくお願いします。