サブテーブルの重複チェック・集計
kintone のサブテーブルは 1 つのレコード内に複数の行データを持てる便利な機能ですが、標準機能だけでは「同じ商品コードの行を登録させたくない」「合計金額をリアルタイムで表示したい」といった業務要件を満たせないことがあります。
この記事では、サブテーブルに対する重複チェック、合計値の自動集計、行数制限など、実務でよく求められるバリデーション・集計パターンを JavaScript で実装する方法を紹介します。
サブテーブル内の重複チェック
保存時に重複を検出してエラーにする
サブテーブル内で、特定のフィールド(例: 商品コード)の値が重複している場合に保存をブロックする実装です。
(() => {
'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 &&
の条件によって空の値は重複チェックの対象外としています。空行も重複としてエラーにしたい場合は、この条件を除外してください。
複数フィールドの組み合わせで重複チェック
「商品コード」と「サイズ」の組み合わせなど、複数フィールドの組み合わせで重複を検出する場合のパターンです。
(() => {
'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;
});
})();
合計値の自動集計
サブテーブルの数値フィールドを合計する
サブテーブル内の数値フィールド(例: 金額)を合計して、テーブル外のフィールドにリアルタイムで反映させる実装です。
(() => {
'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.テーブルのフィールドコード)は、行の追加・削除・行内のフィールド値変更のいずれでも発火します。これにより、ユーザーの操作にリアルタイムで追従した集計が可能です。
小計と消費税を含む明細集計
実務でよくある、小計(単価 × 数量)の自動計算と、合計金額に消費税を加えたパターンです。
(() => {
'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);
})();
小計
フィールドはテーブル内のフィールドとテーブル外のフィールドの両方でフィールドコードが同じ名前にならないように注意してください。上記の例では、テーブル内の行ごとの小計とテーブル外の全体の小計が別のスコープであるため問題ありませんが、混乱を避けるためにテーブル内は
行小計 のように命名を分けることを推奨します。
行数の制限
最大行数を制限する
サブテーブルに追加できる行数の上限を設けたい場合のパターンです。
(() => {
'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 行以上の入力を必須にする場合のパターンです。
(() => {
'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;
});
})();
行ごとのフィールドバリデーション
各行の必須チェック
サブテーブルの各行に対して、特定のフィールドが入力されているかチェックするパターンです。行番号付きのエラーメッセージで、どの行に問題があるか分かりやすく表示します。
(() => {
'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
のエラー表示エリアに複数行のメッセージとして表示されます。行番号を含めることで、ユーザーがどの行を修正すべきか一目で分かります。
応用: 他アプリのマスターデータとの整合性チェック
サブテーブルに入力された商品コードが、マスターアプリに存在するかどうかを非同期で検証するパターンです。
(() => {
'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を使えば、マスターアプリとの整合性チェックなど非同期バリデーションも可能