掲題の仕組みの作り方を在庫管理で例示する。
入出庫アプリに kintone REST API Client と
以下のJavaScriptを適用すれば、上述の仕様を実現できる。
(() => {
'use strict';
// 備品コードのルックアップ参照先のアプリID(在庫アプリのID)を取得
const INVENTORY_APP_ID = kintone.app.getLookupTargetAppId('備品コード');
// kintone REST API Client のインスタンスを生成
const client = new KintoneRestAPIClient();
// テーブル内の備品コード重複をチェックする関数
const checkDuplicateItemCodes = (tableRows) => {
// 備品コードをキーとする行番号の配列
const itemCodeGroups = new Map();
// 前回のエラーメッセージをクリア
tableRows.forEach(row => row.value.備品コード.error = null);
// 備品コードのグループ分けを開始
tableRows.forEach((row, i) => {
// 備品コードが空文字でない行のみ処理を続行
const itemCode = row.value.備品コード.value;
if (itemCode?.trim()) {
// 初回出現の備品コードの場合は新しい配列を作成
if (!itemCodeGroups.has(itemCode)) itemCodeGroups.set(itemCode, []);
// 該当する備品コードの配列に現在の行番号を追加
itemCodeGroups.get(itemCode).push(i);
}
});
// 重複が発生しているかどうかのフラグ
let hasDuplicate = false;
// 各備品コードグループを検査
for (const [, indices] of itemCodeGroups) {
// 同一備品コードが2行以上存在したら重複ありと判定
if (indices.length > 1) {
// 重複している全ての行にエラーメッセージを設定
indices.forEach(index => {
tableRows[index].value.備品コード.error = '備品コードが重複しています。';
});
// 重複フラグを真に設定
hasDuplicate = true;
}
}
return hasDuplicate;
};
// 追加画面が表示されたときの処理
kintone.events.on(['app.record.create.show'], (event) => {
// 在庫列を非表示に設定
kintone.app.record.setFieldShown('在庫', false);
// 入出庫テーブルの全行で繰越在庫列を編集不可にする
event.record.入出庫.value.forEach(row => row.value.繰越在庫.disabled = true);
return event;
});
// 入庫,出庫の値が変更されたとき、入出庫テーブル全行の入庫,出庫に同じ処理をする
kintone.events.on(['app.record.create.change.入庫', 'app.record.create.change.出庫'],
(event) => {
event.record.入出庫.value.forEach(row => {
['入庫', '出庫'].forEach(field => {
let value = row.value[field].value;
// 値の全角数字を半角数字に変換
if (typeof value === 'string') {
value = value.replace(/[0-9]/g, s =>
String.fromCharCode(s.charCodeAt(0) - 0xFEE0));
row.value[field].value = value;
}
// 前回のエラーメッセージをクリア
row.value[field].error = null;
// 値が存在し、かつ自然数でない場合はエラーメッセージを設定
if (value && (!/^\d+$/.test(value) || Number(value) < 1)) {
row.value[field].error = '自然数を入力してください。';
}
});
});
return event;
});
// 入出庫テーブルの行数が変わったときの処理
kintone.events.on(['app.record.create.change.入出庫'], (event) => {
const tableRows = event.record.入出庫.value;
// 備品コードの重複チェックを実行
checkDuplicateItemCodes(tableRows);
// 処理の競合を避けるため、重複チェック完了後に繰越在庫の非活性化を実行
setTimeout(() => {
const currentRecord = kintone.app.record.get();
currentRecord.record.入出庫.value.forEach(row => row.value.繰越在庫.disabled = true);
kintone.app.record.set(currentRecord);
}, 0);
return event;
});
// 備品フィールドの値が変更されたときの処理
kintone.events.on(['app.record.create.change.備品'], (event) => {
// 変更された行のデータを取得
const changedRowData = event.changes.row;
// 変更された行のデータが存在しない場合は処理を終了
if (!changedRowData?.value) return event;
// 値が変わった行の行番号を特定
const tableRows = event.record.入出庫.value;
const rowIndex = tableRows.findIndex(row =>
row.value.備品.value === changedRowData.value.備品.value);
// 該当する行が見つからない場合は処理を終了
if (rowIndex === -1) return event;
// 変更された行の備品の値を取得
const changedRow = tableRows[rowIndex];
const selectedItem = changedRow.value.備品.value;
// 備品コードの重複チェックを実行
checkDuplicateItemCodes(tableRows);
// 備品が選択されていない場合は繰越在庫をクリアして処理を終了
if (!selectedItem) {
changedRow.value.繰越在庫.value = '';
return event;
}
// 在庫アプリから該当備品の在庫を取得
const inventoryQuery = `備品 = "${selectedItem}"`;
client.record.getRecords({
app: INVENTORY_APP_ID,
query: inventoryQuery
// 繰越在庫に取得した在庫の値を設定
}).then(inventoryResponse => {
const currentRecord = kintone.app.record.get();
if (currentRecord.record.入出庫.value[rowIndex]) {
const stockValue = inventoryResponse.records.length > 0
? inventoryResponse.records[0].在庫.value || '0'
: '0';
currentRecord.record.入出庫.value[rowIndex].value.繰越在庫.value = stockValue;
kintone.app.record.set(currentRecord);
}
}).catch((error) => {
event.error = error.message;
});
return event;
});
// レコードを追加したときの処理
kintone.events.on('app.record.create.submit', async (event) => {
const record = event.record;
const stockMovements = record.入出庫.value;
try {
// 備品コードの重複チェックを実行
if (checkDuplicateItemCodes(stockMovements)) {
// 重複があったらエラーを出して処理を中止
event.error = '備品コードの重複があります。';
return event;
}
// エラー行を記録するための配列
const emptyRows = []; // 備品コード未選択の行
const invalidNumberRows = []; // 入庫か出庫に自然数以外の値がある行
const noMovementRows = []; // 入庫も出庫も0か空文字の行
const bulkRequests = []; // 一括処理用のリクエスト配列
// 入出庫テーブルの全行に処理
for (let i = 0; i < stockMovements.length; i++) {
// 処理中の行の値を取得
const movement = stockMovements[i];
const itemName = movement.value.備品コード.value;
const inStockValue = movement.value.入庫.value;
const outStockValue = movement.value.出庫.value;
// 備品コードが未入力の場合はエラー行として記録してから次の行の処理へ移行
if (!itemName?.trim()) {
emptyRows.push(i + 1); // 1行目の行番号が内部的には0なので+1
continue;
}
// 入庫と出庫の値を数値に変換(空文字は0とする)
const inStock = parseInt(inStockValue) || 0;
const outStock = parseInt(outStockValue) || 0;
// 入庫と出庫のバリデーション
['入庫', '出庫'].forEach(field => {
// 値が存在し、かつ自然数でない場合はエラーにする
const value = movement.value[field].value;
if (value && (!/^\d+$/.test(value.toString().trim()) || parseInt(value) < 1)) {
movement.value[field].error = '自然数を入力してください。';
invalidNumberRows.push(i + 1); // エラー行の行番号を記録
// 値が正常な場合はエラーをクリア
} else {
movement.value[field].error = null;
}
});
// 入庫も出庫も0の場合はエラー行として記録して次の行の処理へ移行
if (inStock === 0 && outStock === 0) {
noMovementRows.push(i + 1);
continue;
}
// 在庫アプリから該当備品の現在の在庫の値を取得
const inventoryQuery = `備品コード = "${itemName}"`;
const inventoryResponse = await client.record.getRecords({
app: INVENTORY_APP_ID,
query: inventoryQuery
});
const inventoryRecord = inventoryResponse.records[0];
const currentStock = parseInt(inventoryRecord.在庫.value) || 0;
// 繰越在庫に取得した在庫の値を設定
movement.value.繰越在庫.value = currentStock.toString();
// 取得した在庫 + 入庫 - 出庫 で新しい在庫数を計算
const newStock = currentStock + inStock - outStock;
// 新しい在庫数がマイナスになる場合はエラーとする
if (newStock < 0) {
const itemDisplayName = movement.value.備品.value;
const currentAvailableStock = currentStock + inStock;
event.error = `${itemDisplayName}の在庫が不足しています。` +
`繰越在庫+入庫: ${currentAvailableStock} 出庫: ${outStock}`;
return event;
}
// 在庫フィールドに新しい在庫数を設定
movement.value.在庫.value = newStock.toString();
// 在庫アプリの在庫の値も同数に更新するリクエスト
bulkRequests.push({
method: 'PUT',
api: '/k/v1/record.json',
payload: {
app: INVENTORY_APP_ID,
id: inventoryRecord.$id.value,
record: { 在庫: { value: newStock.toString() } },
revision: inventoryRecord.$revision.value // 楽観ロック用のリビジョン
}
});
}
// 備品コード未選択の行が存在する場合のエラー
if (emptyRows.length > 0) {
event.error = `${emptyRows.join(',')}行目の備品コードが選択されていません。`;
return event;
}
// 入庫か出庫に自然数以外が入力された行が存在する場合のエラー
if (invalidNumberRows.length > 0) {
event.error = `入庫、出庫には自然数しか入力できません。`;
return event;
}
// 入庫も出庫も0の行が存在する場合のエラー
if (noMovementRows.length > 0) {
event.error = `${noMovementRows.join(',')}行目の入庫と出庫が両方とも空になっています。`;
return event;
}
// 入出庫アプリにレコード追加するためのリクエスト
bulkRequests.push({
method: 'POST',
api: '/k/v1/record.json',
payload: {
app: kintone.app.getId(),
record: record
}
});
// リクエストを一括処理
const bulkResp = await client.bulkRequest({ requests: bulkRequests });
// 一括処理完了後、追加されたレコードの詳細画面にリダイレクト
location.href = '/k/' + kintone.app.getId() + '/show#record=' +
bulkResp.results[bulkResp.results.length - 1].id;
// デフォルトのレコード追加処理をキャンセル
return false;
} catch (error) {
event.error = error.message;
return event;
}
});
// レコードを削除したときの処理
kintone.events.on(['app.record.index.delete.submit',
'app.record.detail.delete.submit'], async (event) => {
try {
const record = event.record;
const stockMovements = record.入出庫.value;
// 一括処理用のリクエスト配列
const bulkRequests = [];
// 入出庫テーブルの全行を処理
for (const movement of stockMovements) {
const itemCode = movement.value.備品コード.value;
const inStock = parseInt(movement.value.入庫.value) || 0;
const outStock = parseInt(movement.value.出庫.value) || 0;
// 在庫アプリから該当備品のレコードを検索
const inventoryQuery = `備品コード = "${itemCode}"`;
const inventoryResponse = await client.record.getRecords({
app: INVENTORY_APP_ID,
query: inventoryQuery
});
// 該当備品のレコードが存在しない場合は次の行へ
if (inventoryResponse.records.length === 0) continue;
// レコード削除後の在庫の値を計算(現在の在庫 - 入庫 + 出庫)
const inventoryRecord = inventoryResponse.records[0];
const currentStock = parseInt(inventoryRecord.在庫.value) || 0;
const restoredStock = currentStock - inStock + outStock;
// レコード削除後の在庫の値がマイナスになる場合はエラー
if (restoredStock < 0) {
const itemDisplayName = movement.value.備品.value;
event.error = `${itemDisplayName}の在庫を元に戻せません。` +
`在庫アプリの在庫: ${currentStock}, 出庫-入庫: ${inStock - outStock}`;
return event;
}
// 在庫を削除するレコードが追加される前の値に復元するためのリクエスト
bulkRequests.push({
method: 'PUT',
api: '/k/v1/record.json',
payload: {
app: INVENTORY_APP_ID,
id: inventoryRecord.$id.value,
record: { 在庫: { value: restoredStock.toString() } },
revision: inventoryRecord.$revision.value
}
});
}
// 入出庫アプリのレコードを削除するためのリクエスト
bulkRequests.push({
method: 'DELETE',
api: '/k/v1/records.json',
payload: {
app: kintone.app.getId(),
ids: [record.$id.value],
revisions: [record.$revision.value]
}
});
// リクエストを一括処理
if (bulkRequests.length > 0) {
await client.bulkRequest({ requests: bulkRequests });
// 詳細画面からレコード削除した場合は一覧画面にリダイレクト
if (location.pathname.includes('/show')) {
location.href = '/k/' + kintone.app.getId() + '/';
} else {
// 一覧画面からレコード削除した場合はリロード
location.reload();
}
// デフォルトの削除処理をキャンセル
return false;
}
} catch (error) {
event.error = error.message;
return event;
}
});
})();