サブテーブルの重複チェック・集計

にメンテナンス済み

kintone のサブテーブルは 1 つのレコード内に複数の行データを持てる便利な機能ですが、標準機能だけでは「同じ商品コードの行を登録させたくない」「合計金額をリアルタイムで表示したい」といった業務要件を満たせないことがあります。

この記事では、サブテーブルに対する重複チェック合計値の自動集計行数制限など、実務でよく求められるバリデーション・集計パターンを JavaScript で実装する方法を紹介します。

テーブル操作の基本
kintoneのテーブル(サブテーブル)フィールドをJavaScriptカスタマイズで操作する方法を詳しく解説します。テーブルのデータ構造から、行の追加、更新、削除まで、実践的なサンプルコードと共に紹介します。

サブテーブル内の重複チェック

保存時に重複を検出してエラーにする

サブテーブル内で、特定のフィールド(例: 商品コード)の値が重複している場合に保存をブロックする実装です。

subtable-duplicate-check.js
(() => {
  'use strict';

  const events = [
    'app.record.create.submit',
    'app.record.edit.submit',
    'mobile.app.record.create.submit',
    'mobile.app.record.edit.submit',
  ];

  kintone.events.on(events, (event) => {
    const record = event.record;
    const table = record['明細テーブル'].value;

    // サブテーブル内の商品コードを収集
    const codes = table.map((row) => row.value['商品コード'].value);

    // 重複を検出
    const duplicates = codes.filter((code, index) => {
      return code && codes.indexOf(code) !== index;
    });

    if (duplicates.length > 0) {
      const uniqueDuplicates = [...new Set(duplicates)];
      event.error = `商品コードが重複しています: ${uniqueDuplicates.join(', ')}`;
    }

    return event;
  });
})();
空の行について

上記のコードでは、code && の条件によって空の値は重複チェックの対象外としています。空行も重複としてエラーにしたい場合は、この条件を除外してください。

複数フィールドの組み合わせで重複チェック

「商品コード」と「サイズ」の組み合わせなど、複数フィールドの組み合わせで重複を検出する場合のパターンです。

subtable-composite-duplicate.js
(() => {
  'use strict';

  const events = [
    'app.record.create.submit',
    'app.record.edit.submit',
  ];

  kintone.events.on(events, (event) => {
    const record = event.record;
    const table = record['明細テーブル'].value;

    // 複数フィールドの値を結合してキーを生成
    const keys = table.map((row) => {
      const code = row.value['商品コード'].value || '';
      const size = row.value['サイズ'].value || '';
      return `${code}_${size}`;
    });

    // 重複を検出
    const duplicates = keys.filter((key, index) => {
      return key !== '_' && keys.indexOf(key) !== index;
    });

    if (duplicates.length > 0) {
      event.error = '商品コードとサイズの組み合わせが重複しています';
    }

    return event;
  });
})();

合計値の自動集計

サブテーブルの数値フィールドを合計する

サブテーブル内の数値フィールド(例: 金額)を合計して、テーブル外のフィールドにリアルタイムで反映させる実装です。

subtable-sum.js
(() => {
  'use strict';

  /**
   * サブテーブルの指定フィールドの合計を計算する
   * @param { Object[] } table - サブテーブルの value 配列
   * @param { string } fieldCode - 合計対象のフィールドコード
   * @returns { number } 合計値
   */
  const calculateSum = (table, fieldCode) => {
    return table.reduce((sum, row) => {
      const value = Number(row.value[fieldCode].value) || 0;
      return sum + value;
    }, 0);
  };

  /**
   * 合計値をレコードに反映する
   * @param { Object } event - kintone イベントオブジェクト
   * @returns { Object } event
   */
  const updateTotal = (event) => {
    const record = event.record;
    const table = record['明細テーブル'].value;

    // 合計金額を計算してフィールドに設定
    record['合計金額'].value = calculateSum(table, '金額');

    return event;
  };

  // 画面表示時・テーブル行変更時に合計を更新
  const showEvents = [
    'app.record.create.show',
    'app.record.edit.show',
  ];

  const changeEvents = [
    'app.record.create.change.明細テーブル',
    'app.record.edit.change.明細テーブル',
  ];

  kintone.events.on(showEvents, updateTotal);
  kintone.events.on(changeEvents, updateTotal);
})();
チェック

テーブルのフィールド値変更イベント(change.テーブルのフィールドコード)は、行の追加・削除・行内のフィールド値変更のいずれでも発火します。これにより、ユーザーの操作にリアルタイムで追従した集計が可能です。

小計と消費税を含む明細集計

実務でよくある、小計(単価 × 数量)の自動計算と、合計金額に消費税を加えたパターンです。

subtable-invoice.js
(() => {
  'use strict';

  const TAX_RATE = 0.1; // 消費税率 10%

  const calculate = (event) => {
    const record = event.record;
    const table = record['明細テーブル'].value;

    let subtotal = 0;

    // 各行の小計を計算
    table.forEach((row) => {
      const unitPrice = Number(row.value['単価'].value) || 0;
      const quantity = Number(row.value['数量'].value) || 0;
      const rowTotal = unitPrice * quantity;

      row.value['小計'].value = rowTotal;
      subtotal += rowTotal;
    });

    // 合計・消費税・税込合計を設定
    record['小計'].value = subtotal;
    record['消費税'].value = Math.floor(subtotal * TAX_RATE);
    record['合計金額'].value = subtotal + Math.floor(subtotal * TAX_RATE);

    return event;
  };

  const showEvents = [
    'app.record.create.show',
    'app.record.edit.show',
  ];

  const changeEvents = [
    'app.record.create.change.明細テーブル',
    'app.record.edit.change.明細テーブル',
  ];

  kintone.events.on(showEvents, calculate);
  kintone.events.on(changeEvents, calculate);
})();
チェック

小計 フィールドはテーブル内のフィールドとテーブル外のフィールドの両方でフィールドコードが同じ名前にならないように注意してください。上記の例では、テーブル内の行ごとの小計とテーブル外の全体の小計が別のスコープであるため問題ありませんが、混乱を避けるためにテーブル内は 行小計 のように命名を分けることを推奨します。

行数の制限

最大行数を制限する

サブテーブルに追加できる行数の上限を設けたい場合のパターンです。

subtable-row-limit.js
(() => {
  'use strict';

  const MAX_ROWS = 20;

  const events = [
    'app.record.create.submit',
    'app.record.edit.submit',
  ];

  kintone.events.on(events, (event) => {
    const record = event.record;
    const table = record['明細テーブル'].value;

    if (table.length > MAX_ROWS) {
      event.error = `明細テーブルの行数は最大${MAX_ROWS}行までです(現在: ${table.length}行)`;
    }

    return event;
  });
})();

最小行数を保証する

少なくとも 1 行以上の入力を必須にする場合のパターンです。

subtable-min-rows.js
(() => {
  'use strict';

  const events = [
    'app.record.create.submit',
    'app.record.edit.submit',
  ];

  kintone.events.on(events, (event) => {
    const record = event.record;
    const table = record['明細テーブル'].value;

    // 有効な行(商品コードが入力されている行)をカウント
    const validRows = table.filter((row) => row.value['商品コード'].value);

    if (validRows.length === 0) {
      event.error = '明細テーブルに少なくとも1行は商品を入力してください';
    }

    return event;
  });
})();

行ごとのフィールドバリデーション

各行の必須チェック

サブテーブルの各行に対して、特定のフィールドが入力されているかチェックするパターンです。行番号付きのエラーメッセージで、どの行に問題があるか分かりやすく表示します。

subtable-row-validation.js
(() => {
  'use strict';

  const events = [
    'app.record.create.submit',
    'app.record.edit.submit',
  ];

  kintone.events.on(events, (event) => {
    const record = event.record;
    const table = record['明細テーブル'].value;
    const errors = [];

    table.forEach((row, index) => {
      const rowNumber = index + 1;
      const code = row.value['商品コード'].value;
      const quantity = row.value['数量'].value;

      if (!code) {
        errors.push(`${rowNumber}行目: 商品コードが未入力です`);
      }

      if (!quantity || Number(quantity) <= 0) {
        errors.push(`${rowNumber}行目: 数量は1以上を入力してください`);
      }
    });

    if (errors.length > 0) {
      event.error = errors.join('\n');
    }

    return event;
  });
})();
チェック

エラーメッセージを改行(\n)で区切ると、kintone のエラー表示エリアに複数行のメッセージとして表示されます。行番号を含めることで、ユーザーがどの行を修正すべきか一目で分かります。

応用: 他アプリのマスターデータとの整合性チェック

サブテーブルに入力された商品コードが、マスターアプリに存在するかどうかを非同期で検証するパターンです。

subtable-master-check.js
(() => {
  'use strict';

  /**
   * マスターアプリから商品コードの一覧を取得する
   * @returns { Promise<string[]> } 商品コードの配列
   */
  const fetchMasterCodes = async () => {
    const params = {
      app: 10, // マスターアプリのID
      query: 'order by 商品コード asc limit 500',
      fields: ['商品コード'],
    };

    const response = await kintone.api(
      kintone.api.url('/k/v1/records.json', true),
      'GET',
      params
    );

    return response.records.map((record) => record['商品コード'].value);
  };

  const events = [
    'app.record.create.submit',
    'app.record.edit.submit',
  ];

  kintone.events.on(events, async (event) => {
    const record = event.record;
    const table = record['明細テーブル'].value;

    // マスターデータを取得
    const masterCodes = await fetchMasterCodes();

    // 各行の商品コードをマスターと照合
    const invalidCodes = [];
    table.forEach((row) => {
      const code = row.value['商品コード'].value;
      if (code && !masterCodes.includes(code)) {
        invalidCodes.push(code);
      }
    });

    if (invalidCodes.length > 0) {
      event.error = `マスターに存在しない商品コードがあります: ${invalidCodes.join(', ')}`;
    }

    return event;
  });
})();
チェック

非同期処理を使用するため、コールバック関数に async を付けています。kintone は Promise を返すイベントハンドラーを正しく待機します。

まとめ

  • サブテーブル内の重複は、map で値を収集し filter + indexOf で検出できる
  • 複数フィールドの組み合わせで重複チェックする場合は、値を結合したキーを生成して比較する
  • change.テーブルフィールドコード イベントで行の追加・削除・値変更をリアルタイムに検知できる
  • reduce を使えば、サブテーブルの数値フィールドの合計を簡潔に計算できる
  • 行数の制限(最大・最小)は submit イベントでテーブルの length をチェックする
  • 行番号付きのエラーメッセージを使うと、ユーザーが修正すべき行を特定しやすい
  • async/await を使えば、マスターアプリとの整合性チェックなど非同期バリデーションも可能

関連記事

テーブル操作の基本
kintoneのテーブル(サブテーブル)フィールドをJavaScriptカスタマイズで操作する方法を詳しく解説します。テーブルのデータ構造から、行の追加、更新、削除まで、実践的なサンプルコードと共に紹介します。
submit イベントのバリデーション
kintone の submit イベント(create.submit / edit.submit)を使って、レコードの保存前にカスタムバリデーションを実装する方法を解説します。フィールド単位のエラー表示、複数フィールドの相関チェック、非同
フィールド値の変更イベント
kintone JavaScript カスタマイズにおけるフィールド値変更イベント(app.record.create.change.フィールドコード)の使い方を解説します。ドロップダウン・ラジオボタン・チェックボックスなどの変更を検知し、
レコードの取得(複数件)
kintone REST APIを使用して、レコードを複数件取得する方法を紹介します。レコードIDを指定して、レコード情報を取得することができます。

練習問題

サブテーブル内の重複チェックで、空の値を除外しつつ重複を検出する際に最も適切なコードはどれですか?

サブテーブルの change イベントが発火するタイミングとして正しいものはどれですか?

サブテーブルの合計値を計算するために最も適しているメソッドはどれですか?

#kintone #JavaScript #TypeScript